define([ 'js/events' + $E.jsv,
		'js/customer' + $E.jsv,
		'js/objective' + $E.jsv,
		'js/softphone/calltype',
		'js/softphone/workinfo',
		'js/softphone/pdstype' ],
	function(Events, Customer, Objective, CallType, WorkInfo, PDSType) {

		class Project {
			constructor(projId, projName, projToken, projDigest, groupId, groupName, roles) {
				/**
				 * 项目ID
				 *
				 * @property id
				 * @type {string}
				 */
				this.id = projId;
				this.pubSqlMaxRows = 3000;

				this.events = Events.create(projId);
				/**
				 * 项目名称
				 *
				 * @property name
				 * @type {string}
				 */
				this.name = projName;
				/**
				 * 项目当前是否激活
				 *
				 * @property state
				 * @type {boolean}
				 */
				this.state = false;
				/**
				 * 项目是否初始化完成
				 *
				 * @property initialized
				 * @type {boolean}
				 */
				this.initialized = false;

				/**
				 * 项目登录凭据
				 * @property token
				 * @type {string}
				 */
				this.token = projToken;
				this.digest = projDigest;
				this.epidSql = $E.staff.epidSql;

				this.groupId = groupId;
				this.groupName = groupName;
				this.roles = roles;
				this.terminal = '';

				/**
				 * 项目数据库连接池
				 *
				 * @property dbPool
				 * @type {string}
				 */
				this.dbPool = '';
				/**
				 * 数据库类型表
				 */
				this.dbtypes = {};
				/**
				 * 服务器命令缓存
				 */
				this.srvcmds = {};
				/**
				 * 全局命令组缓存
				 */
				this.globalcmds = {};

				/**
				 * 系统参数表
				 *
				 * @property params
				 * @type {map}
				 */
				this.params = {};
				this.jobs = {
					'FAX' : {
						id : 'FAX',
						clazz : 'job_fax',
						icon: '[s]fax',
						type : 1,
						desc : $F.lang('jobs.name.fax'),
						alertType : 3
					},
					'MAIL' : {
						id : 'MAIL',
						clazz : 'job_mail',
						icon: '[s]envelope',
						type : 2,
						desc : $F.lang('jobs.name.mail'),
						alertType : 3
					},
					'OB' : {
						id : 'OB',
						clazz : 'job_ob',
						icon: '[s]phone-square',
						type : 3,
						desc : $F.lang('jobs.name.ob'),
						alertType : 3
					},
					'WO' : {
						id : 'WO',
						clazz : 'job_wo',
						icon: '[s]briefcase',
						type : 4,
						desc : $F.lang('jobs.name.wo'),
						alertType : 3
					},
					'SMS' : {
						id : 'SMS',
						clazz : 'job_sms',
						icon: '[s]comments',
						type : 5,
						desc : $F.lang('jobs.name.sms'),
						alertType : 3
					},
					'WEB' : {
						id : 'WEB',
						clazz : 'job_web',
						icon: '[b]internet-explorer',
						type : 6,
						desc : $F.lang('jobs.name.msg'),
						alertType : 3
					}
				};

				/**
				 * 业务组件表
				 * 	addin: {
				 * 		id: {string} 组件ID
				 * 		name: {string} 组件名称
				 * 		type: {boolean} 组件类型
				 * 		multiple: {boolean} 是否允许多实例
				 * 	}
				 *
				 * @property addins
				 * @type {array}
				 */
				this.addins = [];
				this.i18n = {};

				/**
				 * 市场活动缓存表
				 * 	objectiveType: {
				 * 		id: 市场活动ID,
				 * 		name: {string} 市场活动名称
				 * 		multiple: {boolean} 市场活动是否允许多实例
				 * 		addinId: {string} 组件ID
				 * 		addinCmtId : {string} 组件的CmtId
				 * 	}
				 *
				 * @property objectiveTypes
				 * @type {array}
				 */
				this.objectiveTypes = [];
				/**
				 * saas带epid表的市场活动缓存
				 */
				this.objectiveTypesSaas = [];

				/**
				 * 工单三的framemap
				 * woFrameMap {
				 * 		id: 市场活动ID（其实是mapId）
				 * 		name: 市场活动名称
				 * 		multiple: 是否允许多实例
				 * 		addinId: 关联组件的ID
				 * 		addinCmtId: 关联组件的CmtId
				 * 		workorderFrameId
				 * }
				 *
				 * @private
				 * @type {Array}
				 */
				this.woFrameMaps = [];

				/**
				 * 项目菜单
				 * @property menus
				 * @private
				 * @type {array}
				 */
				this.menus = [];
				this.launchers = [];

				/**
				 * 用来记录eventMenu
				 * @private
				 * @type {object}
				 */
				this.eventMenu = {};

				/**
				 * SQL查询结果全局缓存
				 *
				 * 	cache : {
				 * 		key: {string} 缓存数据的主键 cacheId,
				 * 		value: {object} 缓存数据
				 * 	}
				 *
				 * @property cache
				 * @type {object}
				 */
				this.cache = {};

				/**
				 * 个性化数据缓存
				 * @property storage
				 * @type {object}
				 */
				this.storage = {};

				/**
				 * 是否在工作中的标识
				 * @type {boolean}
				 */
				this.workDuration = false;

				/**
				 * 用来标识是否在通话中
				 * @type {boolean}
				 */
				this.callDuration = false;

				/**
				 * 当前的callInfo和workInfo
				 * callInfo  原始的callInfo
				 * callType
				 * callStartTime
				 * callEndTime
				 * workStartTime
				 * workEndTime
				 * eventGuid
				 * workStatus
				 * pdsType
				 * callStart 用来标识是否是电话开始
				 * @type {object}
				 */
				this.cwInfo = {};

				/**
				 * 实例对象缓存
				 * @property moduleInstanceMap
				 * @private
				 * @type {object}
				 */
				this.moduleInstanceMap = {};
				//是否允许发送客户端日志
				this.clientLoggerEnabled = false;

				//用来缓存当前项目的customer的所有字段
				this.customerFieldsCache = {
					fields : [],
					fieldsMap : {},
					selectColumn : ''
				};
				// 需要脱敏的customer表字段
				this.customerEncFieldsMasker = {};
				/**
				 * Project当前用户（对每个Project唯一）
				 * @property customer
				 * @type {customer}
				 */
				this.customer = Customer.create(this);

				/**
				 * Project当前联系人（对每个Project唯一）
				 */
				this.contactCustomer = Customer.create(this);

				/**
				 * 项目上的公共属性，用于跨动态页面传递数据
				 */
				this.attributes = {};
				/**
				 * 自定义的菜单参数
				 * @type {{}}
				 */
				this.customMenuParams = {};

				this.currentStaffObject = false;
				this.currentRolegroupObject = false;
				this.trace = {
					enable: false,
					cacheSize: 10,
					cache: [],
					sqlKey: 'ECore.UserTrace.add'
				};

				// 创建菜单区
				this.qvMenu = $('<div class="qvMenu wrapper" />').appendTo($("#qv_body"));

				// 创建任务区
				this.jobContainer = $('#jobContainer');

				const proj = this,
					staticArea = $('#mainBody .staticarea'),
					ul = $('<ul style=\'border-radius:0\' />');

				// 创建作业tab区
				this.tabs = $('<div id="' + projId + 'Tabs" class="project_tabs" style="flex:1; border:0; height:0; border-radius:0; display:none" />');
				staticArea.before(this.tabs);
				this.tabs.append(ul);
				// 创建框架页面TAB组件
				this.tabs.xtabs({
					heightStyle : "fill",
					scrollable: true,
					changeOnScroll: false,
					activate : function(event, ui) {
						const id = ui.newTab.attr('data-id');
						proj.currentTabId = id;
						$.each(proj.tabOrders, function(i, v) {
							if (v == id) {
								proj.tabOrders.splice(i, 1);
								return false;
							}
						})
						proj.tabOrders.push(id);

						// 使用nextTick，避免第一次打开时候，instance还没有创建完
						$F.nextTick(function() {
							const instance = ui.newTab.data('instance');
							if (instance && (instance.type === 'DYN' || instance.type === 'WO')) {
								instance.active();
							}
						});
						//console.log('active:' + proj.currentTabId, proj.tabOrders)
					}
				});
				this.getNav().sortable({
					axis : "x",
					delay : 300,
					stop : function() {
						proj.tabs.xtabs("refresh");
					}
				});

				$('#sideBar').hover(function(){
				}, function(){
					if ($('#mainContainer').hasClass('slideMenu'))
						$('#sideBar').hide();
				})
				// if ($E.query !== 'emb' && $E.query !== 'mf') {
				// TODO: menuGrab
				// const menuGrab = $('<a href="javascript:;" class="menuGrab far fa-fw fa-bars" />').on('mouseover', function (e) {
				// 	e.preventDefault();
				// 	$('#sideBar').show('slide', 100);
				// });
				// ul.append(menuGrab);
				// }

				$(window).resize(function(e) {
					proj.tabs.xtabs("refresh");
				});

				this.tabOrders = [];
			}

			lockTabs (lock) {
				this.tabs.xtabs("option", "disabled", lock);
			}

			/**
			 * 激活/隐藏当前项目
			 * @method active
			 * @param state {boolean} true=激活,false=隐藏
			 */
			active (state) {
				const proj = this;
				proj.state = state;
				if (state) {
					proj.qvMenu.show();
					proj.tabs.show();
					proj.tabs.xtabs("refresh");
					$("header .js_group").text(this.groupName);
					if ($E.staff.epid)
						$("header .c_me .c_me_proj").text('@' + $E.staff.epid);
					else
						$("header .c_me .c_me_proj").text('@' + this.name);

					if ($E.staff.curProject !== proj.id) {
						if (proj.initialized)
							proj.jobAndMarquee();
						Ajax.post({
							url : $E.url + '/switchProject_',
							data : {
								projId : proj.id
							},
							async : false,
							success : function(data) {
								if (data.code < 0)
									$F.err('Project - active', data.message);else {
									$.each($E.staff.projects, function() {
										if (this.id == proj.id) {
											$E.staff.current = this;
											$E.staff.curProject = proj.id;
										}
									})
								}
							}
						})
					}
					$('header a.proj_switch').each(function(i, v) {
						v = $(v);
						v.find('i').toggleClass('fa-dot-circle', v.attr('data-id') == proj.id);
					})
				} else {
					proj.qvMenu.hide();
					proj.tabs.hide();
					$('#mainMarguee').hide();
					$("#mainContainer").removeClass('marquee');
					if ($E.refreshMarqueeHandler)
						clearTimeout($E.refreshMarqueeHandler);
				}
			}

			jobAndMarquee () {
				let proj = this,
					msch = parseInt(proj.getParam('MARQUE') || 0),
					jobsch = parseInt(proj.getParam('JOBSCH') || 0),
					flyerbox = $('#mainMarquee .flyerbox'),
					jobContainer = $('#jobContainer');

				$('footer').toggle(!!(msch || jobsch));
				jobContainer.toggle(jobsch > 0);
				flyerbox.toggle(msch > 0);
				if (msch > 0) {
					if (flyerbox.length) {
						flyerbox.marquee({
							click2remove : false
						}).clear();
					}
					// 刷新跑马灯
					proj.refreshMarquee(msch);
				}
				// 刷新任务
				if (jobsch)
					proj.setJobItems(jobsch);
			}

			init (asyncJobs) {
				var proj = this;
				asyncJobs.push(function(callback) {
					proj.initParameters(function() {
						callback(null, 'parameters');
					})
				});
				asyncJobs.push(function(jobCallback) {
					$E.async.parallel([function(callback) {
						proj.initClientLogger(function() {
							callback(null, 'clientLogger');
						})
					}, function(callback) {
						proj.initCache(function() {
							callback(null, 'caches');
						});
					}, function(callback) {
						proj.initAddins(function() {
							callback(null, 'addins');
						})
					}, function(callback) {
						// SAAS模式，且登录的并非主库
						if ($E.dataService.saas && $E.staff.epidDbPool) {
							// saas不需要初始化objectiveType，没有地方用saas且用工单2了
							setTimeout(function(){
								callback(null, 'objectiveTypes');
							}, 0);
						} else {
							proj.initObjectiveTypes(function() {
								callback(null, 'objectiveTypes');
							})
						}
					}, function(callback) {
						proj.initWoFrameMaps(function() {
							callback(null, 'woFrameMaps');
						})
					}, function(callback) {
						proj.initCustomerFields(function() {
							callback(null, 'customerFields');
						})
					}, function(callback) {
						proj.initI18n(function() {
							callback(null, 'i18n');
						});
					}, function(callback) {
						proj.initDbMask(function() {
							callback(null, 'dbMask');
						});
					}], function(err, result) {
						if (err)
							$F.err(proj.name, err);
						else
							$F.log(proj.name, 'Initialize succeed', result);

						if ($E.query !== 'portal' && $E.query !== 'woa' && $E.query !== 'emb' && $E.query !== 'mf') {
							proj.initMenus();
							proj.initWebQuery()
						}
						proj.initWorkBtns();
						proj.initEvents();
						proj.initElasticSearch();
						proj.initGlobalEvent();
						proj.afterInit();
						jobCallback();
					})
				})
			}

			initClientLogger (callback) {
				var proj = this;
				$E.Global.checkCache($CONST.CacheName.CLIENT_LOGGER_UPLOAD).then(ret => {
					if (ret) {
						proj.clientLoggerEnabled = ret === 'true';
						$F.nextTick(callback);
					} else {
						$F.dataService('SystemDo2', {
							data : $F.sqlbean('Admin.clientlogger.uploadAllow', proj.token, [ $E.staff.id ]),
							digest : proj.digest,
							success : function(data) {
								if (data.code > 0) {
									if (data.value[0].count > 0) {
										if(data.value[0].fields[0] && data.value[0].fields[0][0] > 0)
											proj.clientLoggerEnabled = true;
									}
									$E.Global.saveCache($CONST.CacheName.CLIENT_LOGGER_UPLOAD, proj.clientLoggerEnabled);
								}
							},
							always : function() {
								callback();
							}
						});
					}
				});
			}

			/*
			 * 初始化系统参数
			 */
			initParameters (callback) {
				var proj = this;
				$F.log(proj.name, 'Initialize system parameters');
				var doInitParameters = function() {
					var maxrow = parseInt(proj.params['MAXROW']);
					if (!isNaN(maxrow))
						proj.pubSqlMaxRows = maxrow;

					// 找到系统参数中定义的默认工作任务的报警形式
					if (proj.params['JOBALT']) {
						var cs = proj.params['JOBALT'].split(','),
							matches;
						$.each(cs, function(i, v) {
							v = v.trim().toUpperCase();
							matches = /(\w+)\s*=\s*(\d)/g.exec(v);
							if (matches) {
								if (proj.jobs[matches[1]]) {
									proj.jobs[matches[1]].alertType = parseInt(matches[2]);
								}
								// console.log(proj.jobs[matches[1]]);
							} else
								$F.err('Project - initParameters', '不合法的JOBALT表达式：' + v)
						})
					}
					proj.veSupport = parseInt(proj.params['VESUPP'] || '0');

					// Create personal profile edit menu with parameter MYPROF
					var myprof = proj.params['MYPROF'];
					if (myprof) {
						$('header #c_user>li.myprofile').remove();
						var cs = myprof.split('||'), title = cs[1] || '个人信息', icon = cs[2] || 'user-circle';
						var link = $('<li class="myprofile"><a href="javascript:;"><i class="fa-fw ' + $F.fas(icon) + '"></i> ' + title + '</a></li>');
						$('header #c_user>li:eq(2)').before(link);
						link.on('click', function(e){
							e.preventDefault();
							proj.openAddinDyn(cs[0], title, {}, false, 'DYN', proj.veSupport);
						})
					}
				};

				$E.Global.checkCache($CONST.CacheName.SYSC_PARAMETER).then(ret => {
					if (ret) {
						ret = JSON.parse(ret);
						proj.params = ret;
						doInitParameters();
						$F.nextTick(callback);
					} else {
						proj.params = {};
						var makeParameters = function(arr, saas) {
							$.each(arr, function(i, v) {
								v[0] = v[0].toUpperCase().trim();
								//console.log(v[0])
								var value = ($F.isNull(v[1]) ? '' : v[1].trim());
								if (proj.params[v[0]] === undefined)
									proj.params[v[0]] = value;
								else {
									if (saas) {
										proj.params[v[0]] = value;
									}
									if (!!v[2])
										proj.params[v[0]] = value;
								}
							});
						};

						// 读取主库配置
						$F.dataService('SystemDo2', {
							data : $F.sqlbean('System.parameters', proj.token, [], -99),
							digest : proj.digest,
							async: true,
							success : function(data) {
								if (data.code < 1) {
									$F.alert(data.message);
								} else {
									if (data.value[0].count >= 0) {
										makeParameters(data.value[0].fields);
										if ($E.dataService.saas) {
											// 读取SAAS库配置
											$F.dataService('SystemDo2', {
												data : $F.sqlbean('System.parameters.saas', proj.token, [], -99),
												digest : proj.digest,
												async: false,
												success : function(data) {
													if (data.code < 1) {
														$F.alert(data.message);
													} else {
														if (data.value[0].count >= 0) {
															makeParameters(data.value[0].fields, true);
														} else {
															$F.err('Project - initParameters', $F.lang('db.error') + data.value[0].key);
														}
													}
												}
											})
										}
										doInitParameters();
									} else {
										$F.err('Project - initParameters', $F.lang('db.error') + data.value[0].key);
									}
								}
								$E.Global.saveCache($CONST.CacheName.SYSC_PARAMETER, proj.params);
								callback();
							}
						})
					}
				})

			}

			/*
			 * 初始化系统组件
			 */
			initAddins (callback) {
				var proj = this;
				$F.log(proj.name, 'Initialize sysc_addins');
				$E.Global.checkCache($CONST.CacheName.SYSC_ADDINS).then(ret => {
					if (ret) {
						ret = JSON.parse(ret);
						proj.addins = ret;
						$F.nextTick(callback);
					} else {
						// SQL: select ADDINNAME, ADDINDESC, HTMLTYPE, MULTIPLE, PLUGINNOTES from SYSC_ADDINS
						$F.dataService('SystemDo2', {
							data : $F.sqlbean('System.addins', proj.token, []),
							digest : proj.digest,
							async: true,
							success : function(data) {
								//console.log(data);
								if (data.code < 1)
									$F.alert(data.message);
								else {
									if (data.value[0].count < 0) {
										$F.err('Project - initAddins', $F.lang('db.error') + data.value[0].key);
									} else {
										proj.addins = [];
										$.each(data.value[0].fields, function(i, v) {
											proj.addins.push({
												id : v[0].toUpperCase(),
												name : v[1],
												type : v[2],
												multiple : v[3],
												notes : v[4]
											});
										});
										// console.log(proj.addins);
									}
								}
								$E.Global.saveCache($CONST.CacheName.SYSC_ADDINS, proj.addins);
								callback();
							}
						})
					}
				});
			}

			/*
			 * 初始化市场活动缓存
			 */
			initObjectiveTypes (callback) {
				var proj = this;
				$F.log(proj.name, 'Initialize objective type');
				$E.Global.checkCache($CONST.CacheName.OBJECTIVETYPES).then(ret => {
					if (ret) {
						ret = JSON.parse(ret);
						proj.objectiveTypes = ret;
						$F.nextTick(callback);
					} else {
						$F.dataService('SystemDo2', {
							data : $F.sqlbean('System.objectiveTypes', proj.token, []),
							digest : proj.digest,
							async: true,
							success : function(data) {
								if (data.code < 1) {
									$F.alert(data.message);
								} else {
									if (data.value[0].count >= 0) {
										proj.objectiveTypes = [];
										$.each(data.value[0].fields, function(i, v) {
											proj.objectiveTypes.push({
												id : v[0].trim().toUpperCase(), // 市场活动ID
												name : v[1], // 市场活动名称
												multiple : v[2], // 是否允许多实例
												addinId : v[3].trim().toUpperCase(), // 关联组件的ID
												addinCmtId : v[4].trim() // 关联组件的CmtId
											});
										});
										// console.log(proj.objectiveTypes);
									} else {
										$F.err('Project - initObjectiveTypes', $F.lang('db.error') + data.value[0].key);
									}
								}
								$E.Global.saveCache($CONST.CacheName.OBJECTIVETYPES, proj.objectiveTypes);
								callback();
							}
						})
					}
				});

			}

			initWoFrameMaps (callback) {
				var proj = this;
				$F.log(proj.name, 'Initialize woFrameMap');
				$E.Global.checkCache($CONST.CacheName.WO_FRAME_MAP).then(ret => {
					if (ret) {
						ret = JSON.parse(ret);
						proj.woFrameMaps = ret;
						$F.nextTick(callback);
					} else {
						$F.dataService('SystemDo2', {
							data : $F.sqlbean('System.woFrameMaps', proj.token, [ $E.staff.epidSql ]),
							digest : proj.digest,
							async: true,
							success : function(data) {
								if (data.code < 1)
									$F.alert(data.message);
								else {
									if (data.value[0].count >= 0) {
										proj.woFrameMaps = [];
										$.each(data.value[0].fields, function(i, v) {
											proj.woFrameMaps.push({
												id : v[0].trim().toUpperCase(), // 市场活动ID（其实是mapId）
												name : v[1], // 市场活动名称
												multiple : v[2], // 是否允许多实例
												addinId : v[3].trim().toUpperCase(), // 关联组件的ID
												addinCmtId : v[4].trim(), // 关联组件的CmtId
												workorderFrameId : v[5] // 真正的市场活动id（objectivetype表的id）
											});
										});
									} else {
										$F.err('Project - initWoFrameMaps', $F.lang('db.error') + data.value[0].key);
									}
								}
								$E.Global.saveCache($CONST.CacheName.WO_FRAME_MAP, proj.woFrameMaps);
								callback();
							}
						})
					}
				});
			}

			/*
			 * 缓存用户表结构字段
			 */
			initCustomerFields (callback) {
				// 获取用户表结构字段
				var proj = this;
				$F.log(proj.name, 'Initialize customer fields');
				$E.Global.checkCache($CONST.CacheName.CUSTOMER_FIELDS).then(ret => {
					if (ret) {
						ret = JSON.parse(ret);
						proj.customerFieldsCache = ret;
						$F.nextTick(callback);
					} else {
						$F.dataService('SystemDo2', {
							data : $F.sqlbean('ECore.Customer.selectCustomerForField', proj.token, [ $E.staff.epidSql ]),
							digest : proj.digest,
							async: true,
							success : function(data) {
								if (data.code < 1)
									$F.alert(data.message);
								else {
									if (data.value[0].count >= 0) {
										var columns = data.value[0].columns;
										proj.customerFieldsCache.fields = columns;
										columns.forEach(function(col) {
											proj.customerFieldsCache.fieldsMap[col.toLowerCase()] = '';
										});
										proj.customerFieldsCache.selectColumn = columns.join(",");
									} else {
										$F.err('Project - initCustomerFields', $F.lang('db.error') + data.value[0].key);
									}
								}
								$E.Global.saveCache($CONST.CacheName.CUSTOMER_FIELDS, proj.customerFieldsCache);
								callback();
							}
						})
					}
				});
			}

			/**
			 * 加载项目中后台定义的多语言资源
			 */
			initI18n (callback) {
				var a = this;
				$F.log(a.name, 'Initialize i18n');
				$E.Global.checkCache($CONST.CacheName.I18N).then(ret => {
					if (ret) {
						ret = JSON.parse(ret);
						a.i18n = ret;
						$F.nextTick(callback);
					} else {
						var key = 'System.i18n.get', epid = 'MAINDB', dbPool = '';
						if ($E.dataService.saas) {
							dbPool = a.params['CFGDBP'] || '';
							epid = ($E.staff.epid || 'PUBLIC');
						}
						$F.dataService('SystemDo2', {
							data: $F.sqlbean2(key, a.token, dbPool, ['CUSMENU', epid]),
							digest: a.digest,
							success: function(data) {
								if (data.code > 0) {
									if (data.value[0].count > 0) {
										a.i18n = JSON.parse(data.value[0].fields[0][0]);
										$E.Global.saveCache($CONST.CacheName.I18N, a.i18n);
									}
								} else {
									$F.err(data.message);
								}
								callback();
							}
						})
					}
				});
			}

			isAdminRole () {
				for (var i = 0; i < this.roles.length; i++) {
					if (this.roles[i].id == 'G00001')
						return true;
				}
				return false;
			}

			/*
			 * 初始化用户功能菜单
			 */
			initMenus () {
				const proj = this, roles = [];

				if ($E.query === 'portal') {
					return;
				}
				$F.log(proj.name, 'Initialize menus');
				$.each(proj.roles, function() {
					roles.push("'" + this.id + "'");
				})
				if (roles.length == 0)
					roles.push("''");

				var rolegroupType = '';
				$F.dataService('SystemDo2', {
					data: $F.sqlbean('System.RoleGroupType.Get', proj.token, [$E.staff.epidSql, proj.groupId]),
					async: false,
					digest: proj.digest,
					successResult: function(data) {
						if (data[0].count > 0) {
							rolegroupType = data[0].fields[0][0];
						}
					}
				})

				var reqData, saasTemplates = {};
				if ($E.dataService.saas && $E.staff.epid) {
					// SAAS模式，且当前登录的并非主库, 先根据套餐读取模版菜单
					$F.dataService('SystemDo2', {
						data: $F.sqlbean2({MENU:'System.Tree.Menu.QX.SAAS.bySuite'}, proj.token, proj.params['CFGDBP']||'', []),
						digest: proj.digest,
						async: false,
						successResult: function(data) {
							saasTemplates = $F.makeJsonArrayMap(data[0], 'MENU_ID');
						}
					})

					reqData = $F.sqltree({MENU:'System.Tree.Menu.QX.SAAS'}, proj.token, [ proj.groupId, roles.join(','), rolegroupType ], 'MENU_ID', 'MENUPARENT_ID', 1);
				} else {
					reqData = $F.sqltree({MENU:'System.Tree.Menu.QX'}, proj.token, [ proj.groupId, roles.join(','), rolegroupType ], 'MENU_ID', 'MENUPARENT_ID', 1);
				}
				$F.dataService('SystemDo4', {
					data : reqData,
					digest : proj.digest,
					async: false,
					successResult : function(data) {
						// console.log(data);
						var cl = function(children, arr) {
							$.each(children, function(i, v) {
								var menu = {
									id : v.v[0],
									name : $F.i18n(v.v[1], proj.i18n[$E.lang]),
									type : v.v[2],
									addinId : v.v[3],
									addinCmt : v.v[4],
									otId : v.v[5],
									singleton : parseInt(v.v[6]) || 0,
									comment : v.v[7],
									parameter : v.v[8],
									pId : v.v[9],
									background : v.v[10],
									icon : v.v[11],
									tip : v.v[12],
									pluginNotes : v.v[13],
									matchCti : v.v[14],
									callNatureId : v.v[15],
									callNatureGroupId : v.v[16],
									onlySupervisor : parseInt(v.v[17]),
									keepOpen : parseInt(v.v[18]),
									hidden : parseInt(v.v[19]),
									startup : parseInt(v.v[20]),
									source: v.v[21],
									folder : v.folder,
									children : []
								}
								if (menu.source) {
									var source = saasTemplates[menu.source];
									if (source) {
										menu.type = source.HTMLTYPE
										menu.addinId = source.ADDINID,
											menu.addinCmt = source.ADDINCMT,
											menu.otId=source.ADDINOTID
									} else {
										$F.err('丢失菜单模版：' + menu.name + '[' + menu.id + ' -> ' + menu.source + ']');
									}
								}
								if (menu.onlySupervisor && $E.staff.supervisor <= proj.getParam('ISSUPE'))
									return;
								if (v.children != null)
									cl(v.children, menu.children)
								arr.push(menu);
							});
						};
						proj.menus.length = 0;
						cl(data.tree.children, proj.menus);

						// load menu shortcuts
						$F.dataService('SystemDo2', {
							data: $F.sqlbean('System.menu.launcher.get', proj.token, []),
							async: false,
							digest: proj.digest,
							successResult: function(data) {
								if (data[0].count > 0) {
									try {
										proj.launchers = JSON.parse(data[0].fields[0][0]);
									} catch(e) {
										console.error(e);
									}
								}
							}
						})
						proj.initShortcuts();

						//console.log(proj.menus);
						$F.cleanTree(proj.menus);
						if ($E.query !== "emb" && $E.query !== 'portal' && $E.query !== 'mf') {
							proj.drawMenus();
						}
					}
				})
			}

			drawMenus () {
				const proj = this;
				const draw = function(items, menubox, level, handleEvent) {
					var x = 0;
					$.each(items, function(i, v) {
						//考虑comment如果有[HIDE]，则隐藏treeItem (第一层)
						if (v.hidden)
							return;
						var iconStyle = v.icon || (v.folder ? 'folder-open' : 'leaf');
						var mi = $('<dl class="menu">').appendTo(menubox),
							dt = $('<dt class="menu_title" />').appendTo(mi),
							fs,
							childDiv,
							fsicon;
						var icon = iconStyle.split('#'),
							prefix = '',
							iconDom,
							c = '';

						if (icon.length > 1)
							c = ' style="color:#' + icon[1] + '"';


						if ($E.theme === 'gfSkin'){
							iconDom = '';
						} else {
							if (icon[0].startWith('@')) {
								prefix += ' fa-spin';
								icon[0] = icon[0].substring(1);
								iconDom = '<i class="menuicon ' + $F.fas(icon[0]) + prefix + ' fa-fw"' + c + '></i>'
							} else if (icon[0].startsWith('$')) {
								const url = icon[0].substring(1);
								iconDom = '<img class="menuimg" src="' + url + '">'
							} else if (icon[0].startsWith('%') && $E.theme === 'gfSkin') {
								const url = icon[0].substring(1);
								iconDom = '<svg class="icon svg-icon" aria-hidden="true" style="width:2em;height:2em;"><use xlink:href="#icon-' + url + '"></use></svg>'
							} else {
								iconDom = '<i class="menuicon ' + $F.fas(icon[0]) + ' fa-fw"' + c + '></i>'
							}
						}

						fsicon = '<a href="javascript:;" title="' + v.name + '" style="margin-left:' + (level * 2) + 'em">' + iconDom + '<span>' + v.name + '</span></a>';
						if (v.folder || !v.type) {
							fs = $('<i class="menu_foldset"></i>');
							$(fsicon).append(fs).appendTo(dt).click(function(e) {
								e.stopPropagation();
								$(document).click();
								$('.menu_title').removeClass('active selected');
								$(this).parent().toggleClass('active');
								childDiv.toggle();
								fs.toggleClass('expand', childDiv.is(":visible"))
							})
							if (v.children) {
								childDiv = $('<div class="menu_childs" title="' + v.name + '"/>').appendTo(mi).hide();
								if (draw(v.children, childDiv, level + 1))
									x ++;
								else
									mi.remove();
							} else {
								mi.remove();
							}
						} else {
							dt.addClass("clickable");
							const a = $(fsicon).appendTo(dt).click(function(e) {
								e.preventDefault();
								$('.menu_title').removeClass('active selected');
								$(e.currentTarget).parents('.menu_title').addClass('selected')
								handleEvent && handleEvent()
								proj.onMenu(v);
							});
							if (v.tip)
								a.attr('title', v.tip);
							x ++;
						}
					})
					return x > 0;
				}

				const drawTwoCol = function(menus, menuWrapper) {
					let expanded = false
					const collapse = function() {
						$('#mainContainer').addClass('collapsed');
						expanded = false
						$.cookie('two-col.collapsed', true)
					}
					const expand = function(expandState) {
						$('#mainContainer').removeClass('collapsed');
						if (expandState) {
							expanded = true
							$.cookie('two-col.collapsed', '')
						}
					}
					const header = $('<div class="header"><i class="far fa-bars tow-col-expanded"></i>' +
						'<i class="far fa-bars tow-col-collapsed"></i></div>')
					$('.tow-col-expanded', header).on('click', function() {
						collapse()
					})
					$('.tow-col-collapsed', header).on('click', function() {
						expand(true)
					})
					const container = $('<div class="two-col-container" />');
					const firstCol = $('<div class="first-col menu-col" />').appendTo(container);
					const secondCol = $('<div class="second-col menu-col" />').appendTo(container)

					const drawItem = function(menu, wrapper, clickHandle) {
						const link = $('<a href="javascript:;" class="menu-item" />');
						const iconStyle = menu.icon || (menu.folder ? 'folder-open' : 'leaf');
						const iconInfo = iconStyle.split('#');
						let icon, style, prefix = '';
						if (iconInfo.length > 1)
							style = ' style="color:#' + iconInfo[1] + '"';

						if (iconInfo[0].startWith('@')) {
							prefix += ' fa-spin';
							iconInfo[0] = iconInfo[0].substring(1);
							icon = '<i class="icon ' + $F.fas(iconInfo[0]) + prefix + ' fa-fw"' + style + '></i>'
						} else if (iconInfo[0].startsWith('$')) {
							const url = iconInfo[0].substring(1);
							icon = '<img class="menuimg" src="' + url + '" />'
						}  else if (iconInfo[0].startsWith('%') && $E.theme === 'gfSkin') {
							const url = iconInfo[0].substring(1);
							icon = '<svg class="icon svg-icon" aria-hidden="true"><use xlink:href="#icon-' + url + '"></use></svg>'
						} else {
							icon = '<i class="icon ' + $F.fas(iconInfo[0]) + ' fa-fw"' + style + '></i>'
						}
						const label = $('<span class="title" />').text(menu.name);
						link.append(icon)
							.append(label)
							.appendTo(wrapper)
							.on('click', function(e) {
								clickHandle && clickHandle(e)
							})
					}
					menus.forEach((menu) => {
						drawItem(menu, firstCol, function(e) {
							e.preventDefault();
							secondCol.mCustomScrollbar('destroy')
							$('.menu-item', firstCol).removeClass('active')
							$(e.currentTarget).addClass('active')
							secondCol.empty();
							expand()

							// console.log(menu)
							if (! menu.children) {
								proj.onMenu(menu);
								return;
							}
							draw(menu.children, secondCol, 0, function() {
								!expanded && collapse()
							});
							secondCol.mCustomScrollbar({
								theme : "minimal-dark",
								autoHideScrollbar : true,
								scrollInertia : 100,
								mouseWheelPixels : 150
							})
						})
					})
					menuWrapper.css({
						display: 'flex',
						'flex-direction': 'column'
					}).append(header).append(container);
					firstCol.mCustomScrollbar({
						theme : "minimal-dark",
						autoHideScrollbar : true,
						scrollInertia : 100,
						mouseWheelPixels : 150
					})
				}

				if ($('#body').hasClass('twocol')) {
					drawTwoCol(proj.menus, proj.qvMenu.empty());
				} else {
					proj.qvMenu.mCustomScrollbar('destroy');
					draw(proj.menus, proj.qvMenu.empty(), 0);
					proj.qvMenu.mCustomScrollbar({
						theme : "minimal-dark",
						autoHideScrollbar : true,
						scrollInertia : 100,
						mouseWheelPixels : 150
					})
				}
			}

			/**
			 * 初始化cache
			 */
			initCache (callback) {
				$E.async.parallel([
					callback => {
						$E.Global.checkCache($CONST.CacheName.FREE_DATE).then(ret => {
							if (ret) {
								ret = JSON.parse(ret);
								this.putCache($CONST.CacheName.FREE_DATE, ret);
								callback(null, ret);
							} else {
								this.commonQuery("ECore.CodeTable.c_freedate", [], undefined, -99).then(ret => {
									var freeDate = {};
									if (!$F.isNull(ret)) {
										$.each(ret.value[0].fields, function(i, fd) {
											try {
												freeDate[new Date(fd).format('YYYY-MM-dd')] = fd;
											} catch (e) {}
										});
									}
									this.putCache($CONST.CacheName.FREE_DATE, freeDate);
									$E.Global.saveCache($CONST.CacheName.FREE_DATE, freeDate);
									callback(null, freeDate);
								})
							}
						});
					},
					callback => {
						$E.Global.checkCache($CONST.CacheName.CALL_RESULT_REASON).then(ret => {
							if (ret) {
								ret = JSON.parse(ret);
								this.putCache($CONST.CacheName.CALL_RESULT_REASON, ret);
								callback(null, ret);
							} else {
								//SELECT CallResultReason_ID, CallResultReasonDescription, TimeAdd, CloseOB, OBStatus, ObjectiveStatus,FAILUREFLAG,MAXTIMES,objectivetype_id,togrpflag,togrp FROM SYSC_CallResultReason
								this.commonQuery("ECore.CodeTable.sysc_callresultreason", []).then(ret => {
									var crrs = {};
									if (ret) {
										crrs = $F.makeJsonArrayMap(ret.value[0], 'CALLRESULTREASON_ID');
									}
									this.putCache($CONST.CacheName.CALL_RESULT_REASON, crrs);
									$E.Global.saveCache($CONST.CacheName.CALL_RESULT_REASON, crrs);
									callback(null, crrs);
								});
							}
						});
					},
					callback => {
						$E.Global.checkCache($CONST.CacheName.OBJECTIVE_SUB_STATUS).then(ret => {
							if (ret) {
								ret = JSON.parse(ret);
								this.putCache($CONST.CacheName.OBJECTIVE_SUB_STATUS, ret);
								callback(null, ret);
							} else {
								//SELECT CallResultReason_ID, CallResultReasonDescription, TimeAdd, CloseOB, OBStatus, ObjectiveStatus,FAILUREFLAG,MAXTIMES,objectivetype_id,togrpflag,togrp FROM SYSC_CallResultReason
								this.commonQuery('ECore.Codetable.objectivesubstatus', []).then(ret => {
									var osss = {};
									if (ret) {
										osss = $F.makeJsonArrayMap(ret.value[0], 'OBJECTIVESUBSTATUS_ID');
									}
									this.putCache($CONST.CacheName.OBJECTIVE_SUB_STATUS, osss);
									$E.Global.saveCache($CONST.CacheName.OBJECTIVE_SUB_STATUS, osss);
									callback();
								});
							}
						});
					},
					callback => {
						$E.Global.checkCache($CONST.CacheName.ROLE_PASS_QX).then(ret => {
							if (ret) {
								ret = JSON.parse(ret);
								this.putCache($CONST.CacheName.ROLE_PASS_QX, ret);
								callback(null, ret);
							} else {
								//初始化自定义权限
								// select * from rolepassqx{C_EPID} where (staff_id={C_BY} and (rolegroup_id={C_GRP} or rolegroup_id is null)) or (staff_id is null and rolegroup_id={C_GRP}) or (role_id in (''{C1}))
								var rolesStr = '';
								$.each(this.roles, function(i, role) {
									rolesStr += ",'" + role.id + "'";
								});
								this.commonQuery('ECore.Codetable.rolepassqx', [ rolesStr ]).then(ret => {
									var rps = {};
									if (ret) {
										rps = $F.makeJsonArrayMap(ret.value[0], 'ROLEGUID');
									}
									this.putCache($CONST.CacheName.ROLE_PASS_QX, rps);
									$E.Global.saveCache($CONST.CacheName.ROLE_PASS_QX, rps);
									callback(null, rps);
								});
							}
						});
					}
				], (err, result) => {
					if (err)
						$F.err(this.name, err);
					else
						$F.log(this.name, 'Initialize successed', result);
					callback();
				});
			}

			initDbMask (callback) {
				//初始化脱敏masker配置
				if (this.isDbMask()) {
					var proj = this;
					let setupDBMask = (ret) => {
						proj.putCache($CONST.CacheName.DB_MASKER, ret);
						// customer的脱敏信息
						proj.customerEncFieldsMasker = {};
						Customer.encFields.forEach(function(f) {
							var masker = proj.getCacheValue($CONST.CacheName.DB_MASKER, 'CUSTOMER.' + f);
							if (masker) {
								proj.customerEncFieldsMasker[f] = masker;
							}
						});
					};
					$E.Global.checkCache($CONST.CacheName.DB_MASKER).then(ret => {
						if (ret) {
							ret = JSON.parse(ret);
							setupDBMask(ret);
							callback && callback();
						} else {
							this.commonQuery('ECore.Codetable.dbMasker', []).then(ret => {
								var dbMaskers = {};
								if (ret) {
									var data = ret.value[0];
									if (data.count > 0) {
										$.each(data.fields, function(i, field) {
											var bean = {};
											$.each(data.columns, function(k, column) {
												bean[column] = field[k];
											});
											dbMaskers[(bean['TABLE_NAME'] + '.' + bean['COLUMN_NAME']).toUpperCase()] = bean;
										})
									}
								}
								setupDBMask(dbMaskers);
								$E.Global.saveCache($CONST.CacheName.DB_MASKER, dbMaskers);
								callback && callback();
							});
						}
					});
				} else {
					$F.nextTick(callback);
				}
			}

			initTrace() {
				let ENTRAC = this.getParam('ENTRAC');
				if (ENTRAC) {
					try {
						ENTRAC = JSON.parse(ENTRAC);
						this.trace.enable = ENTRAC.enable;
						this.trace.cacheSize = ENTRAC.cacheSize || 10;
					} catch (ignore) {}
				}
			}

			initShortcuts () {
				var proj = this,
					bar = $('#menuBar'),
					newShortcuts = [];

				bar.find('ul').remove();
				loopMenus(proj.menus, function(menu) {
					var bean = proj.launchers.find(function(e) {
						return e.id == menu.id && !menu.hidden;
					})
					if (bean) {
						bean.menu = menu;
						newShortcuts.push(bean);
					}
				})
				if (! newShortcuts.length)
					return;
				newShortcuts = newShortcuts.sort(function(a, b) {
					return a.index - b.index;
				})
				//console.log(newShortcuts)
				var ul = $('<ul class="tinyScroller" />'), li;
				$.each(newShortcuts, function(i, v) {
					li = $('<li />').appendTo(ul);
					$('<a href="javascript:;" title="' + v.name + '"><i class="fa-fw ' + $F.fas(v.icon || 'coffee')  + '"></i></a>')
						.data('menu', v.menu)
						.on('click', function(e) {
							proj.onMenu($(e.currentTarget).data('menu'));
						}).appendTo(li)
				})

				bar.append(ul);
				if (window.Tipped)
					Tipped.create($('li>a', ul), { position:'righttop', radius:false });
				else
					$('li>a', ul).powerTip({
						placement: 'e' // east tooltip position
					})
			}

			initWebQuery () {
				var proj = this, mt = new Date(), jobsch = proj.getParam('JOBSCH');
				// 针对SAAS模式下，无任务刷新需求时不需要初始化webquery
				if (parseInt(jobsch) == 0)
					return;

				$F.log(proj.name, 'Initialize workload query jobs');
				$F.dataService('SystemDo2', {
					data : $F.sqlbean({'JOBANDMSG':'Job.notice.query'}, proj.token, []),
					digest : proj.digest,
					async: false,
					success : function(data) {
						//console.log(data);
						if (data.code > 0) {
							if (data.value[0].count < 0)
								$F.err('Project - initWebQuery', data.value[0].key);
							else {
								$.each(data.value[0].fields, function() {
									//console.log(this);
									proj.jobs[this[0]] = {
										id : this[0],
										type : this[1],
										desc : this[2],
										app : true,
										clazz : 'job_app',
										icon : this[3],
										otid : ($F.isEmpty(this[4]) ? '' : this[4]),
										dynid : this[5],
										plugin: this[11],
										personState : this[6] == '' || (this[6].toUpperCase().indexOf('P') != -1),
										groupState : this[6] == '' || (this[6].toUpperCase().indexOf('G') != -1),
										params : this[7],
										conflect : this[8],
										alertType : this[9],
										veSupport : this[10]
									}
								})
								if ($E.debug)
									console.log('Project jobs defined: ', proj.jobs);
							}
						} else {
							$F.err('Project - initWebQuery', data.message);
						}
					}
				})
			}

			/**
			 * 初始化elasticsearch，如果配置了相关系统参数的话，才会去加载
			 * @param callback
			 */
			initElasticSearch (callback) {
				const proj = this;
				$F.log(proj.name, 'Initialize elasticsearch tools');
				require([ 'js/lib/elasticsearch.client' + $E.jsv ], function(esc) {
					esc.init({
						token: proj.token,
						terminal: proj.terminal
					}, function() {
						$F.log('Project - initElasticSearch', 'succeed!');
						callback && callback();
					}, function(error) {
						$F.log('Project - initElasticSearch', 'failed, ', error);
						callback && callback();
					});
					proj.esc = esc;
				})
			}

			initGlobalEvent (callback) {
				const proj = this;
				$F.log(proj.name, 'Initialize global event');
				var gecmd = proj.params['GEVENT'];
				if (gecmd) {
					var instance = {
						id: 'GlobalEvent',
						project: proj,
						DSL: [],
						data: {},
						struct: {
							i18n: {}
						}
					}
					var dc = new Executer(instance);
					dc.doCmdList('globalEvent', [{
						type: 999,
						content: {
							sub: 0,
							cmdId: gecmd
						}
					}], '', {
						INSTANCE: instance
					}, {
						id: 'GlobalEvent',
						namespace: '',
						instance: instance,
						project: proj,
						props : {
							pub : instance.data,
							local : {}
						},
						data : {
							pub : {},
							local : {}
						},
						ctls : {},
						binder : {},
						sqlQueue : {},
						struct: instance.struct
					});
				}
				callback && callback();
			}

			/**
			 * 初始化工作小结和取消按钮
			 */
			initWorkBtns () {
				var proj = this, btnDiv = $("#quickView #qv_workstate");
				btnDiv.find("button.js_ok").on('click', function(e) {
					if (proj.workDuration) {
						proj.callSummary.open();
					} else {
						$F.alert("当前不在工作中");
					}
				});
				btnDiv.find("button.js_cancel").on('click', function(e) {
					if (proj.workDuration) {
						var eventGuid = proj.cwInfo.eventGuid;
						var nowDateStr = $F.getDbTime();
						if (!$F.isEmpty(eventGuid)) {
							var softCommonCall = "customer_guid=" + proj.getCustomerGuid() + "|contact_customer_guid=" + proj.getCurrentContactCustomerGuid() +
								"|customername=" + proj.customer.getValue("customername") + "|CallResult_ID=CANCEL|HandleBy_ID=" + $E.staff.id + "|HandleGroup_ID=" +
								proj.groupId + "|WorkStartTime=" + proj.cwInfo.workStartTime + "|WorkEndTime=" + nowDateStr + "|StringField2=CANCEL" + "|EVENT_GUID=" + eventGuid;
							$F.log("project.cancelBtn.click", "Event update with:" + softCommonCall);
							$E.softphone.simpleCommonCall("setevent", softCommonCall);
						} else {
							$F.log("project.cancelBtn.click", "No event_guid found when cancel work.");
						}
						proj.events.notify($CONST.ModuleEvent.ECM_WORKEND);
						proj.callSummary.callSummaryEnd();
					} else {
						$F.alert("当前不在工作中");
					}
				})
			}

			/**
			 * 初始化项目事件
			 * @method initEvents
			 * @param callback {callback} Async对象回调方法
			 * @return {project} 项目对象
			 */
			initEvents () {
				var proj = this;
				proj.events.register($CONST.EVENT.OPEN_OBJ, $CONST.EVENT.LISTENERID, function(data) {
					if (data.oId) {
						$F.log("Project.Event.OPEN_OBJ", 'openAddinObjByOId: ' + data.oId);
						proj.openAddinObjByOId(data.addinId, data.tabName, data.oId, data.parameter, data.keepOpen);
					} else if (data.otId) {
						$F.log("Project.Event.OPEN_OBJ", 'openAddinObjByOtId: ' + data.otId);
						proj.openAddinObjByOtId(data.addinId, data.tabName, data.otId, data.parameter, data.keepOpen);
					} else {
						$F.err('Project.Event.OPEN_OBJ', "市场活动GUID或者客户数据对象GUID都为空。");
					}
				});
				proj.events.register($CONST.EVENT.OPEN_DYN, $CONST.EVENT.LISTENERID, function(data) {
					$F.log("Project.Event.OPEN_DYN", 'openAddinDyn: ' + data.dynId);
					proj.openAddinDyn(data.dynId, data.tabName, data.parameter, data.keepOpen, data.trigger, data.veSupport == 1);
				});
				proj.events.register($CONST.EVENT.OPEN_WO, $CONST.EVENT.LISTENERID, function(data) {
					if (data.oId) { //根据objective_guid打开工单
						$F.log("Project.Event.OPEN_WO", "openAddinWOByOId with addinId: " + data.addinId + "   tabName: " + data.tabName + "   oId: " + data.oId);
						proj.openAddinWOByOId(data.addinId, data.tabName, data.oId);
					} else if (data.subId) { //根据协办id打开工单
						$F.log("Project.Event.OPEN_WO", "openAddinWOByStepId with addinId: " + data.addinId + "   tabName: " + data.tabName + "   subId: " + data.subId);
						proj.openAddinWOByStepId(data.addinId, data.tabName, data.subId);
					} else if (data.otId) { //根据objectivetype_id打开工单（可能是创建新工单）
						$F.log("Project.Event.OPEN_WO", "openAddinWOByOtId with addinId: " + data.addinId + "   tabName: " + data.tabName + "   otId: " + data.otId);
						proj.openAddinWOByOtId({
							addinId: data.addinId,
							tabName: data.tabName,
							otId: data.otId
						});
					}
				});

				/**
				 * 1.根据objective_guid和taskId打开工单3
				 $project.event.notify(window.$CONST.EVENT.OPEN_WO3, {
						  addinId : 'AddinWO3',
						  tabName : 'xxxx',
						  oId: '',//需要打开的市场活动guid
						  taskId: '', //需要打开的工单任务guid
						  notifyId: '', //相关提醒的guid，非必传
						  parameter: {}, //带入参数
						  dbPool: '', //数据源，非必传
						  readonly: false, // 是否是只读模式
						  continuable: true, // 操作结束时候，是否不自动关闭工单
						  lastInstance: lastInstance, // 上个工单的instance对象（用来实现不关闭工单tab直接加载新工单，仅当上个实例里的动态页面Id和新打开的动态页面Id一致时候）
						  forceProxy: false, // 是否是强制代理
						  tabc: '' // 关联的tabc表名（用来支持根据tabc表记录打开工单）
					 });
				 *
				 * 2.根据objectivetype_id打开工单3，可能是新建工单：
				 $project.event.notify(window.$CONST.EVENT.OPEN_WO3, {
						  addinId : 'AddinWO3',
						  tabName : 'xxxx',
						  otId: '', //需要打开的市场活动类型id
						  customerGuid: '', //可以直接传递某个客户guid，不传递则按原来逻辑获取客户guid
						  stepId: 'xxx', //步骤id，可以从指定步骤新建工单
						  parameter: {}, //带入参数
						  dbPool: '', //数据源，非必传
						  alwaysNew: true, //是否每次都
					 });
				 */
				proj.events.register($CONST.EVENT.OPEN_WO3, $CONST.EVENT.LISTENERID, function(data) {
					if (data.oId) { //根据objective_guid打开工单  taskId可以是空
						if (data.lastInstance) {
							let d = Object.assign({}, data);
							delete d.lastInstance;
							$F.log("Project.Event.OPEN_WO3", "openAddinWO3ByOId with params: " + JSON.stringify(d) + " and lastInstance: " + data.lastInstance.id);
						} else {
							$F.log("Project.Event.OPEN_WO3", "openAddinWO3ByOId with params: " + JSON.stringify(data));
						}
						proj.openAddinWO3ByOId({
							addinId: data.addinId,
							tabName: data.tabName,
							oId: data.oId,
							taskId: data.taskId,
							parameters: data.parameters || data.parameter,
							notifyId: data.notifyId,
							dbPool: data.dbPool,
							readonly: data.readonly,
							continuable: data.continuable,
							lastInstance: data.lastInstance,
							forceProxy: data.forceProxy,
							tabc: data.tabc
						});
					} else if (data.otId) { //根据objectivetype_id打开工单（可能是创建新工单）
						$F.log("Project.Event.OPEN_WO3", "openAddinWOByOtId with addinId: " + JSON.stringify(data));
						proj.openAddinWOByOtId({
							addinId: data.addinId,
							tabName: data.tabName,
							otId: data.otId,
							parameters: data.parameters || data.parameter,
							wo3: true,
							customerGuid: data.customerGuid,
							stepId: data.stepId,
							dbPool: data.dbPool,
							readonly: data.readonly,
							alwaysNew: data.alwaysNew,
							rolegroupId: data.rolegroupId
						});
					}
				});

				proj.events.register($CONST.EVENT.OPEN_DBTOOLS, $CONST.EVENT.LISTENERID, function(data) {
					var addin = proj.getAddin('DBTOOLS');
					if (!addin) {
						$F.alert('表维护组件没有找到')
					} else {
						proj.openAddinPlugin('表维护', addin, 'T#' + data.data);
					}
				});

				//project自己的的CTI事件监听
				proj.events.register($CONST.CTIEvent.CTI_ONCALLSTART, $CONST.EVENT.LISTENERID, function(callInfo) {
					if (callInfo.getValue("callType") == CallType.INBOUND) {
						if (proj.workDuration && !proj.cwInfo.callEstablished) {// 如果已经开始了工作，但是电话没接通，这时候放弃上一个电话，直接开启新一个电话
							$F.log('Project.CallStartHandler', 'Finish last unestablished call: ' + proj.cwInfo.eventGuid);
							// 新电话进来后，软电话已经不支持更新老的event了
							// var softCommonCall = "customer_guid=" + proj.getCustomerGuid() + "|contact_customer_guid=" + proj.getCurrentContactCustomerGuid() +
							// 	"|customername=" + proj.customer.getValue("customername")+"|HandleBy_ID=" +
							// 	$E.staff.id+"|HandleGroup_ID=" + proj.groupId + "|WorkStartTime="+ proj.cwInfo.workStartTime +
							// 	"|WorkEndTime=" + $F.getDbTime() + "|EVENT_GUID=" + proj.cwInfo.eventGuid;
							// $E.softphone.simpleCommonCall("setevent", softCommonCall); // 更新event
							proj.events.notify($CONST.ModuleEvent.ECM_WORKEND); // 直接触发工作结束
						}
					}


					proj.cwInfo.callInfo = callInfo;
					proj.cwInfo.callStartTime = callInfo.getValue("callstarttime");
					proj.cwInfo.callStart = true;
					proj.callDuration = true;
					//do inbound
					proj.cwInfo.callType = callInfo.getValue("callType");
					proj.cwInfo.eventGuid = callInfo.getValue("event_guid");
					proj.cwInfo.agentId = callInfo.getValue("agentId");
					proj.cwInfo.agentState = callInfo.getValue("agentState");
					proj.cwInfo.ani = callInfo.getValue("ANI");
					proj.cwInfo.dnis = callInfo.getValue("DNIS");
					proj.cwInfo.extension = callInfo.getValue("extension");

					var allAttachDatas = callInfo.getValue("allattachdatas");
					$F.log("Project.CallStartHandler", "allAttachDatas: " + allAttachDatas);
					try {
						allAttachDatas = JSON.parse(allAttachDatas);
					} catch (e) {}
					//为了避免随路数据大小写问题，我这里全部大写的key也赋值一遍
					$.each(allAttachDatas, function(key, value) {
						allAttachDatas[key.toUpperCase()] = value;
					});

					var callBack = allAttachDatas["CALLBACK"] == "1";

					$F.log("Project.CallStartHandler", "cwInfo: " + JSON.stringify(proj.cwInfo));
					if ($E.softphone.ivr.callback) {
						if (callBack) {
							$E.softphone.ivr.transferToIVRSuccess(allAttachDatas);
							$F.log("Project.CallStartHandler", "Find block module and callback is true.");
						} else {
							$F.log("Project.CallStartHandler", "Find block module but callback is false. Hence ignore this call.");
						}
					} else {
						if (!callBack) {
							if (proj.cwInfo.callType == CallType.INBOUND) {
								$F.log("Project.CallStartHandler", "---------------------Inbound---------------------");
								try {
									//多项目时，自动切换到对应项目
									if ($E.getActiveProject().id != proj.id) {
										$F.log("Project.CallStartHandler", "Switch project from " + $E.getActiveProject().id + " to " + proj.id);
										for (var key in $E.projects) {
											$E.projects[key].active(false);
										}
										$E.projects[proj.id].active(true);
										$E.events.notify($CONST.EVENT.PROJECT_CHANGED, {
											id : proj.id
										});
										$("header .c_me_proj").text('@' + $E.projects[proj.id].name);
									}

									var OCASUM = proj.getParam("OCASUM");
									if (OCASUM != "0") {
										$F.log("Project.CallStartHandler", "Inbound popup customer module.");
										proj.openCustomerTab();
									}

									//ECore.workInfo.workStatus=WorkInfo.WT_IB;
									var customerGuidFromAttachData = allAttachDatas["CUSTOMER_GUID"];
									if (customerGuidFromAttachData) {
										$F.log("Project.CallStartHandler", "Receive customer_guid from attachData : " + customerGuidFromAttachData);
										var SCCBAD = proj.getParam("SCCBAD");
										if (SCCBAD != "0") {
											$F.log("Set current customer from call_start attachdata: " + customerGuidFromAttachData);
											proj.cwInfo.setCustomerFromAttachData = proj.setCurrentCustomerByGuid(customerGuidFromAttachData);
										}
									}

									//接收来自随路数据的elitetask_guid，直接打开相关工单
									var elitetaskGuidFromAttachData = allAttachDatas["ELITETASK_GUID"];
									if (elitetaskGuidFromAttachData) {
										$F.log("Project.CallStartHandler", "Receive elitetask_guid from attachData : " + elitetaskGuidFromAttachData);
										proj.openAddinWO3ByTaskId(taskId, false);
									}

									var CPOPUP = proj.getParam("CPOPUP");
									if (!$F.isEmpty(CPOPUP)) {
										var menuId = $(CPOPUP).find("menu_id").text();
										if (!$F.isEmpty(menuId)) {
											$F.log("Project.CallStartHandler", "Inbound popup menu:" + menuId);
											for (var i = 0; i < proj.menus.length; i++) {
												var menu = proj.menus[i];
												if (menu.id == menuId) {
													proj.onMenu(menu);
													break;
												}
											}
										}
									}
								} catch (e) {
									$F.err("Project.CallStartHandler", "Inbound error: " + e);
								}
								H5Utils.notifyMe('有新的来电');
								window.focus();
							} else if (proj.cwInfo.callType == CallType.OUTBOUND) {
								$F.log("Project.CallStartHandler", "---------------------Outbound---------------------");
								if (!$F.isNull(proj.customer)) {
									var softCommonCall = "calltype=2|customer_guid=" + proj.getCurrentCalledCustomerGuid() + "|contact_customer_guid=" + proj.getCurrentContactCustomerGuid() +
										"|customername=" + $F.parseEmpty(proj.customer.getValue("customername")) +
										"|HandleBy_ID=" + $E.staff.id + "|HandleGroup_ID=" + proj.groupId + "|WorkStartTime=" + callInfo.getValue("callStartTime") +
										"|EVENT_GUID=" + callInfo.getValue("event_guid");
									$E.softphone.simpleCommonCall("setevent", softCommonCall);
									$F.log("Project.CallStartHandler", "Event update with:" + softCommonCall);
								}
							} else if (proj.cwInfo.callType == CallType.PDS_OUTBOUND) {
								var pdsType = allAttachDatas["PDSTYPE"];
								var pds = allAttachDatas["PDS"];
								var avayaPDS = allAttachDatas["AVAYAPDS"];
								if ($F.equalsIgnoreCase(pdsType.trim(), PDSType.ELITE_PDS)) {
									$F.log("Project.CallStartHandler", "---------------------PDS Inbound---------------------");
									proj.cwInfo.pdsType = PDSType.ELITE_PDS;
								} else if (avayaPDS == "1") {
									$F.log("Project.CallStartHandler", "---------------------Avaya PDS Inbound---------------------");
									proj.cwInfo.pdsType = PDSType.AVAYA_PDS;
									var ANI = allAttachDatas["ANI"];
									var phone1 = allAttachDatas["PHONE1"];
									var phone2 = allAttachDatas["PHONE2"];
									var phone3 = allAttachDatas["PHONE3"];
									var phone4 = allAttachDatas["PHONE4"];
									if (ANI == phone1) {
										proj.cwInfo.pdsPhoneIndex = 1;
									} else if (ANI == phone2) {
										proj.cwInfo.pdsPhoneIndex = 2;
									} else if (ANI == phone3) {
										proj.cwInfo.pdsPhoneIndex = 3;
									} else if (ANI == phone4) {
										proj.cwInfo.pdsPhoneIndex = 4;
									}
								} else if (pds == "1") {
									$F.log("Project.CallStartHandler", "---------------------Aspect PDS Inbound---------------------");
									proj.cwInfo.pdsType = PDSType.ASPECT_PDS;
								} else {
									$F.log("Project.CallStartHandler", "---------------------PDS Inbound with PDSType:" + pdsType + "---------------------");
									proj.cwInfo.pdsType = pdsType;
								}
								try {
									let objectiveGuid = allAttachDatas["OBJECTIVE_GUID"];
									let objectiveTypeId = allAttachDatas["OBJECTIVETYPE_ID"];
									let customerGuid = allAttachDatas["CUSTOMER_GUID"];
									let phoneNumber = allAttachDatas["PHONENUMBER"];
									let taskId = allAttachDatas["ELITETASK_GUID"];
									let OBJTWO = proj.getParam("OBJTWO");
									if (!$F.isEmpty(objectiveGuid)) {
										if (OBJTWO == 1) {
											let ret = proj.commonQuerySync("ECore.Objective.selectDetailByGuid", [ $E.staff.epidSql, objectiveGuid ]);
											if (!$F.isNull(ret) && ret.value[0].count > 0) {
												let otId = ret.value[0].fields[0][1];
												let ot = proj.getObjectiveType(otId);
												if (ot) {
													customerGuid = ret.value[0].fields[0][6];
													if (!$F.isEmpty(customerGuid)) {
														proj.setCurrentCustomerByGuid(customerGuid);
														proj.openAddinObjByOId(ot.addinId, ot.name, objectiveGuid);
													} else {
														$F.err("Project.CallStartHandler", "customer_guid is null on objective " + objectiveGuid);
													}
												} else {
													$F.err("Project.CallStartHandler", "Not found objectiveType with otId: " + otId);
												}
											} else {
												$F.err("Project.CallStartHandler", "Not found objective with oId: " + objectiveGuid);
											}
										} else {
											//工单3下面，优先判断是不是拿到ELITETASK_GUID，如果没有则用objective_guid（兼容之前objective_guid其实就是elitetask_guid的模式）
											if (!taskId)
												taskId = objectiveGuid;
											proj.openAddinWO3ByTaskId(taskId, true);
										}
									} else if (!$F.isEmpty(objectiveTypeId)) {
										if (OBJTWO == 1) {
											let ot = proj.getObjectiveType(objectiveTypeId);
											if (ot) {
												let ret;
												if (!$F.isEmpty(customerGuid)) {
													ret = Customer.checkExists(proj, "ECore.Customer.searchByGuid",[$E.staff.epidSql, customerGuid]);
													if ($F.isEmpty(ret)) {
														Customer.createNewCustomerForPDS(customerGuid, null);
													}
												} else if (!$F.isEmpty(phoneNumber)) {
													if (Verify.isMobile(phoneNumber)) {
														ret = Customer.checkExists(proj, "ECore.Customer.searchByMobile", [phoneNumber]);
													} else {
														ret = Customer.checkExists(proj, "ECore.Customer.searchByPhones", [phoneNumber]);
													}

													if ($F.isEmpty(ret)) {
														customerGuid = ret;
													} else {
														customerGuid = Customer.createNewCustomerForPDS(null, phoneNumber);
													}
												} else {
													customerGuid = Customer.createNewCustomerForPDS(null, phoneNumber);
												}
												proj.setCurrentCustomerByGuid(customerGuid);
												proj.openAddinObjByOtId(ot.addinId, ot.name, objectiveTypeId);
											} else {
												$F.err("Project.CallStartHandler", "Not found objectiveType with otId: " + objectiveTypeId);
											}
										} else {
											let ot = proj.getFrameMap(objectiveTypeId);
											if (ot) {
												proj.openAddinWOByOtId({
													addinId: ot.addinId,
													tabName: ot.name,
													otId: objectiveTypeId,
													menuId: null,
													parameters: {},
													wo3: true
												});
											} else {
												$F.err("Project.CallStartHandler", "Not found mapId : " + objectiveTypeId);
											}
										}
									} else {
										$F.err("Project.CallStartHandler", "Objective_GUID or objectivetype_id is null.");
									}
								} catch (e) {
									$F.err("PDS inbound error:" + e.message);
								}
								//                                EXSoftPhone.focusWin();
							}
						} else {
							$F.err("Project.CallStartHandler", "Error:No block module but callback is true.");
						}
					}
				});

				proj.events.register($CONST.CTIEvent.CTI_ONCALLESTABLISH, $CONST.EVENT.LISTENERID, function() {
					proj.cwInfo.callEstablished = true;
				});

				proj.events.register($CONST.CTIEvent.CTI_ONCALLEND, $CONST.EVENT.LISTENERID, function() {
					proj.cwInfo.callEndTime = $F.getDbTime();
					proj.callDuration = false;
				});

				proj.events.register($CONST.CTIEvent.CTI_ONLOGIN, $CONST.EVENT.LISTENERID, function(sfInfo) {
					$F.log("Project.LoginHandler", "Receive onlogin event from SCTIEvent.");
					//CaveatUtil.agentLogined=true;
					// var AVYPDS = proj.getParam("AVYPDS");
					// if (AVYPDS == "1") {
					// 	var avayaPDSAddJobsWindow:AvayaPDSAddJobsWindow = new AvayaPDSAddJobsWindow();
					// 	PopUpManager.addPopUp(avayaPDSAddJobsWindow,this,true);
					// 	PopUpManager.centerPopUp(avayaPDSAddJobsWindow);
					// }
				});

				proj.events.register($CONST.CTIEvent.CTI_ONCALLACTION, $CONST.EVENT.LISTENERID, function(params) {
					$F.log("Project.CallActionHandler", "Receive OnCallAction event from SCTIEvent : " + params.type);
					var actionType = params.actionType;
					var dest = params.dest;
					if (proj.id == $E.getActiveProject()) {
						if (actionType == "InitTransfer" || actionType == "InitConference" ||
							actionType == "MuteTransfer" || actionType == "SingleStepTransfer") {
							$E.softphone.simpleCommonCall("AddCallData", "customer_guid=" + proj.getCustomerGuid());
						}
					}
				});

				proj.events.register($CONST.ModuleEvent.ECM_WORKSTART, $CONST.EVENT.LISTENERID, function(params) {
					$F.log('Project - [' + proj.id + ']', 'Receive ECM_WORKSTART.');
					proj.cwInfo.workStartTime = $F.getDbTime();
					proj.workDuration = true;
					if (proj.cwInfo.callType == CallType.INBOUND || proj.cwInfo.callType == CallType.SIMULAT_INBOUND) {
						proj.cwInfo.workStatus = WorkInfo.WT_IB;
					} else if (proj.cwInfo.callType == CallType.PDS_OUTBOUND) {
						proj.cwInfo.workStatus = WorkInfo.WT_PDS_OB;
					} else {
						proj.cwInfo.workStatus = WorkInfo.WT_OB;
					}

					if (!$F.isEmpty(params.workStatus)) {
						$E.softphone.notifyWorkStart(params.workStatus);
					} else {
						$E.softphone.notifyWorkStart(proj.cwInfo.workStatus);
					}

					// 显示工作状态按钮，配置成2时候表示单单小结按钮隐藏 配置成3时候表示单单取消按钮隐藏
					var DSWBTN = proj.getParam('DSWBTN');
					if (DSWBTN != '1') {
						var menuDiv = $("#quickView #qv_body"),
							btnDiv = $("#quickView #qv_workstate");
						menuDiv.animate({
							bottom : '50px'
						});
						btnDiv.show().animate({
							height : '45px'
						});
						if (DSWBTN == '2') {
							$("#quickView #qv_workstate .js_ok").hide();
						} else if (DSWBTN == '3') {
							$("#quickView #qv_workstate .js_cancel").hide();
						}
					}
				});

				proj.events.register($CONST.ModuleEvent.ECM_WORKEND, $CONST.EVENT.LISTENERID, function() {
					$F.log('Project - [' + proj.id + ']', 'Receive ECM_WORKEND.');

					// 显示工作状态按钮
					var menuDiv = $("#quickView #qv_body"),
						btnDiv = $("#quickView #qv_workstate");
					menuDiv.animate({
						bottom : '0'
					});
					btnDiv.animate({
						height : '0'
					}).hide();

					proj.cwInfo.workEndTime = $F.getDbTime();
					proj.lastCwInfo = $.extend(true, {}, proj.cwInfo);
					proj.cwInfo = {};
					proj.workDuration = false;
					proj.saveEventMenu();

					$E.softphone.notifyWorkEnd();
				});

				proj.events.register($CONST.WorkLoadNotifyEvent.OB, $CONST.EVENT.LISTENERID, function(pg) {
					proj.outbound.open(pg); //pg 0 代表本人 1 代表本组
				});

				proj.events.registerAll([ $CONST.WorkLoadNotifyEvent.MAIL, $CONST.WorkLoadNotifyEvent.SMS, $CONST.WorkLoadNotifyEvent.FAX ], $CONST.EVENT.LISTENERID, function(pg) {
					var EFLIST = proj.getParam("EFLIST");
					if (EFLIST) {
						proj.openAddinDyn(EFLIST, "传真邮件短信列表", pg, false, 'DYN', proj.veSupport);
					} else {
						$F.alert('未定义EFLIST系统参数，无法进行相应跳转');
					}
				});

				proj.events.register($CONST.WorkLoadNotifyEvent.WO, $CONST.EVENT.LISTENERID, function(pg) {
					const jbdeb = proj.getParam("JBDEB");
					if ($F.isEmpty(jbdeb)) {
						return $F.alert("未读取到系统参数JBDEB");
					}
					const navs = proj.getNavs();
					for (var i = 0; i < navs.length; i++) {
						if ($(navs[i]).data("addinId") === $CONST.ModuleName.ADDIN_WORKORDERSERCH) {
							proj.tabReopenByIndex(i);
							return true;
						}
					}
					const instance = $D.createInstance(proj, jbdeb, '', '', pg, true, 'WO', proj.veSupport);
					if (instance) {
						const tab = proj.tabCreate($CONST.ModuleName.ADDIN_WORKORDERSERCH, $F.lang('wo.wolist'), true, false);
						tab.data("instance", instance);
						tab.data("addinId", $CONST.ModuleName.ADDIN_WORKORDERSERCH);
						$E.progressing = true;
						$D.live(instance, tab.data("container"));
						$E.progressing = false;
					} else
						$F.err('Project - WorkLoadNotifyEvent', '创建实例失败：' + jbdeb);
				});

				//客户资料改变消息，本来在openCustomerTab时候注册，现在需要放到这里，要先于其他动态页面注册这个消息
				proj.events.register($CONST.ModuleEvent.ECM_CUSTOMERCHANGE, 'CustomerObject', function() {
					$F.log("Customer", "receive ECM_CUSTOMERCHANGE");
					proj.showCustomerDetail();
				});

				$F.log(proj.name, "Initialize events");
			}

			afterInit () {
				const proj = this;
				proj.initialized = true;

				if (proj.params['MSGNTY'] === 'noticeOnly')
					$('header ul#c_alert>li.notes').remove();

				if ($E.query !== 'portal' && $E.query !== 'woa' && $E.query !== 'emb' && $E.query !== 'mf') {
					const CHATAD = proj.getParam('CHATAD');
					if (!CHATAD) {
						$('header .c_chat').remove();
					} else {
						proj.chatSwitch = $('header .c_chat').click(function(e) {
							e.preventDefault();
							if (proj.getParam('CHALOC') === 'standalone') {
								window.open('chat', 'CHAT_WIN');
							} else if (proj.getParam('CHALOC') === 'embed') {
								// 嵌入动态页面中，这里不需要处理
							} else {
								if (CHATAD.startWith('others:')) {
									$E.openChatPanel();
									const chatAd = CHATAD.substring(7, CHATAD.length);
									if ($('#chatPanel iframe').length === 0) {
										$('#chatPanel').append('<iframe src="' + $F.addArgumentsToURL(chatAd, proj) + '" allow="microphone;camera" style="width:100%; height:100%"></iframe>');
									}
								} else {
									proj.openChatPlugin($("#chatPanel"));
								}
								// $('#chatPanel .chat').show();
								// $('#chatPanel .chatMonitor').hide();
								$('#chatPanel .videoChat').hide();
							}
						});
					}

					const MONITR = proj.getParam('MONITR');
					if (!MONITR) {
						$('header .c_chat_monitor').remove();
					} else {
						proj.chatSwitch = $('header .c_chat_monitor').click(function(e) {
							e.preventDefault();
							console.log('click!')
							$E.openChatPanel();
							proj.openChatMonitorPlugin($("#chatPanel"))
							$('#chatPanel .chat').hide();
							$('#chatPanel .videoChat').hide();
							$('#chatPanel .chatMonitor').show();
						});
					}

					const videoParams = proj.getParam('VIDEOU');
					if (videoParams) {
						try {
							let videoParamsObj = JSON.parse(videoParams);

							proj.videoChatSwitch = $('header .c_video').click(function(e) {
								$E.events.notify($CONST.SoftphoneEvent.VIDEO_CLICKED);
								$E.openChatPanel();

								if(videoParamsObj.type === 1) {
									if ($('#chatPanel iframe').length === 0) {
										$('#chatPanel').append('<iframe class="videoChat" name="videoChat" src="' + videoParamsObj.url  + '" allow="microphone;camera" style="width:100%; height:100%"></iframe>');
									}
								} else if (videoParamsObj.type === 2) {
									proj.openVideoPlugin($("#chatPanel"))
								}

								$('#chatPanel .chatMonitor').hide();
								$('#chatPanel .chat').hide();
								$('#chatPanel .videoChat').show();
							});

						} catch (e) {

						}

					} else {
						$('header .c_video').remove();
					}

					if (proj.state)
						proj.jobAndMarquee();
				}

				proj.initTrace();
			}

			/**
			 * 获得系统参数值
			 *
			 * @method getParam
			 * @param key {string} 系统参数主键
			 * @param [defaultValue] {any} 当该系统参数不存在时的默认值
			 * @return {string} 系统参数（强制转换为大写字符串）
			 */
			getParam (key, defaultValue) {
				var param = this.params[key.toUpperCase()];
				if (param === undefined)
					return defaultValue;
				return param;
			}

			/**
			 * 获得市场活动对象
			 *
			 * @method getObjectiveType
			 * @param otId {string} 市场活动ID
			 * @return {object} objectiveType
			 */
			getObjectiveType (otId) {
				otId = otId.toUpperCase();
				if ($E.dataService.saas && $E.staff.epidDbPool) {
					for (var i = 0; i < this.objectiveTypesSaas.length; i++) {
						if (this.objectiveTypesSaas[i].id == otId)
							return this.objectiveTypesSaas[i];
					}
				}
				for (var i = 0; i < this.objectiveTypes.length; i++) {
					if (this.objectiveTypes[i].id == otId)
						return this.objectiveTypes[i];
				}
				return undefined;
			}

			getFrameMap (frameMapId) {
				frameMapId = frameMapId.toUpperCase();
				for (var i = 0; i < this.woFrameMaps.length; i++) {
					if (this.woFrameMaps[i].id == frameMapId)
						return this.woFrameMaps[i];
				}
				return undefined;
			}

			/**
			 * 根据组件ID获取查询组件对象
			 * @method getAddin
			 * @param addinId {string} 组件ID
			 * @return {object} 组件对象
			 */
			getAddin (addinId) {
				addinId = addinId.toUpperCase();
				for (var i = 0; i < this.addins.length; i++) {
					if (this.addins[i].id == addinId)
						return this.addins[i];
				}
				return undefined;
			}

			/**
			 * 从全局缓存获取一个数据对象
			 *
			 * @method getCache
			 * @param {string} key 缓存数据的主键
			 * @return 缓存数据对象
			 */
			getCache (key) {
				return ($F.isNull(key) ? undefined : this.cache[key]);
			}

			/**
			 * 向全局缓存放入一个数据对象
			 *
			 * @method putCache
			 * @param {string} key 缓存数据的主键 cacheId
			 * @param {object} data 数据对象
			 */
			putCache (key, data) {
				this.cache[key] = data;
				return this;
			}

			/**
			 * 获取缓存中的值，更具缓存类型和缓存key
			 * @param key
			 * @param id
			 * @returns {*}
			 */
			getCacheValue (key, id) {
				var oneCache = this.cache[key];
				if (!$F.isNull(oneCache)) {
					return oneCache[id];
				}
				return null;
			}

			start () {
				var proj = this;
				if ($E.query === 'woa') {
					proj.openWoSearchTab();
				} else if ($E.query === 'emb' || $E.query === 'mf') {

				} else if ($E.query === 'portal') {
				} else {
					$F.nextTick(function() {
						proj.openCustomer();
						proj.openStartUpMenus();
						proj.openStaticDyn();
					})
				}
			}

			commonQuerySync (sqlKey, params, dbPool, max) {
				var proj = this,
					mt = new Date(),
					result;
				dbPool = dbPool || '';
				$F.logGroup(proj.name + ' - commonQuerySync', sqlKey);
				$F.dataService('SystemDo2', {
					async : false,
					data : $F.sqlbean2(sqlKey, proj.token, dbPool, params, max),
					digest : proj.digest,
					success : function(data) {
						if (data.code < 1)
							$F.err('Project - commonQuerySync', data.message || '数据操作失败: ' + sqlKey);
						if (data.value && data.value[0].count >= 0) {
							result = data;
						}
					},
					always : function() {
						$F.log(proj.name, 'done, took time: ' + (new Date() - mt) + '\'ms', result);
						$F.logGroupEnd();
					}
				});
				return result;
			}

			commonQuery (sqlKey, params, dbPool, max) {
				var proj = this,
					mt = new Date();
				dbPool = dbPool || '';
				$F.log('Project - commonQuery', sqlKey);
				return new Promise(resolve => {
					$F.dataService('SystemDo2', {
						async : true,
						data : $F.sqlbean2(sqlKey, proj.token, dbPool, params, max),
						digest : proj.digest,
						success : function(data) {
							if (data.code < 1) {
								$F.err('Project - commonQuery', data.message || '数据操作失败: ' + sqlKey);
								return resolve();
							}
							if (data.value && data.value[0].count >= 0) {
								return resolve(data);
							}
							resolve();
						},
						fail: function() {
							resolve();
						},
						always : function() {
							$F.log('Project - commonQuery', sqlKey + ' done, took time: ' + (new Date() - mt) + '\'ms');
						}
					});
				});
			}

			/**
			 * 根据sqlkey和sql参数，以传入的主键字段值为key，对应一条记录为value，返回这样的而一个map对象
			 * @param sqlKey
			 * @param params
			 * @param primaryKey
			 * @param dbPool
			 * @returns {{}}
			 */
			queryForMap (sqlKey, params, primaryKey, dbPool) {
				var obj;
				var ret = this.commonQuerySync(sqlKey, params, dbPool);
				if (ret && ret.value[0].count > 0) {
					obj = $F.makeJsonArrayMap(ret.value[0], primaryKey);
				}
				return obj;
			}

			/**
			 * 根据sqlkey和sql参数，直接返回出查询的字符串,此方法类似jdbctemplate的queryforString,用来直接返回SQL查询单个字段的值
			 * @param sqlKey
			 * @param params
			 * @param dbPool
			 * @returns string
			 */
			queryForString (sqlKey, params, dbPool) {
				var str;
				var ret = this.commonQuerySync(sqlKey, params, dbPool);
				if (ret && ret.value[0].count > 0) {
					str = ret.value[0].fields[0][0];
				}
				return str;
			}

			commonUpdateSync (sqlKey, params, dbPool) {
				var proj = this,
					result = 0;
				dbPool = dbPool || '';
				$F.dataService('SystemDo1', {
					async : false,
					data : $F.sqlbean2(sqlKey, proj.token, dbPool, params),
					digest : proj.digest,
					success : function(data) {
						if (data.code < 0)
							$F.err('Project - commonUpdateSync', data.message || '数据操作失败: ' + sqlKey);
						result = 1;
					},
					fail : function(xhr) {
						result = -1;
					}
				});
				return result;
			}

			addEventMenu (menu) {
				if (this.cwInfo.workDuration) {
					var eventGuid = this.cwInfo.eventGuid;
					if (!$F.isEmpty(eventGuid)) {
						var em = {
							event_guid : eventGuid,
							menu_id : menu.id,
							menudescription : menu.name
						};
						this.eventMenu[menu.id] = em;
					}
				}
			}

			addTrace(key, name, type, event) {
				this.trace.cache.push({key, name, type, event});
				if (this.trace.cache.length >= this.trace.cacheSize) {
					const dataRequest = {
						token : this.token,
						dbPool : this.dbPool,
						sqls : []
					};
					//ECore.UserTrace.add = INSERT INTO USER_TRACE_LOG{C_EPID}(TRACE_GUID, TRACE_TYPE, STAFF_ID, ROLEGROUP_ID, ACTION, OBJECT_KEY, OBJECT_DESC, OBJECT_DATA, CREATEDDATE)
					// VALUES(@{C1}S, @{C2}N, @{C_BY}S, @{C_GRP}S, @{C3}S, @{C4}S, @{C5}S, @{C6}S, @{C_NOW}D)
					this.trace.cache.forEach(t => {
						var sql = {
							areaKey: '',
							key : this.trace.sqlKey,
							params : {C1: Guid.raw(), C2: t.type, C3: t.event.type, C4: t.key, C5: t.name, C6: t.event.data, C7: '', C8: ''}
						};
						dataRequest.sqls.push(sql);
					});
					$F.dataService('SystemDo1', {
						async : true,
						data: dataRequest,
						digest : this.digest
					});
					this.trace.cache = [];
				}
			}

			saveEventMenu () {
				const proj = this;
				if (!$.isEmptyObject(this.eventMenu)) {
					const nowDateStr = $F.getDbTime();
					$.each(this.eventMenu, function(i, menu) {
						$F.log("Prject.saveEventMenu", "save eventMenu for event: " + menu.event_guid + " and menu: [" + menu.id + "]" + menu.name);
						proj.commonUpdateSync("ECore.Menu.insertEventMenu", [ $E.staff.epidSql, menu.event_guid, menu.id, menu.name, $E.staff.id, nowDateStr, proj.groupId ]);
					});
				}
				this.eventMenu = {};
			}

			onMenu (menu) {
				//console.log(menu)
				if (menu.folder)
					return;

				if (menu.source) {
					// relation with saas frame menu
					menu = getRelationMenu(menu, this);
				}
				if ($('#mainContainer').hasClass('slideMenu'))
					$('#sideBar').hide();

				// 如果菜单没配置组件，直接不处理
				if (!menu.addinId) return;
				// 获取菜单对应的组件
				const a = this, addin = this.getAddin(menu.addinId);
				if (!addin)
					return $F.alert('组件不存在：' + menu.addinId);
				a.addEventMenu(menu);
				a.addTrace(menu.id, menu.name, 0,{
					type: 'click'
				});
				$F.processAsync(function(hide) {
					if (addin.type === 'URL') { // 组件类型为外部链接
						a.openAddinView(menu.name, addin, menu.addinCmt, menu.parameter, menu.comment, menu.keepOpen);
					} else if (addin.type === 'OBJ') { // 组件类型为市场活动
						var ot = a.getObjectiveType(menu.otId);
						if (!ot)
							$F.alert('市场活动不存在：' + menu.otId);
						else {
							if (menu.parameter === 'INTERNAL_CUSTOMER')
								a.openCustomerTab(ot.addinCmtId);
							else {
								a.openAddinObj(menu.name, addin, ot, undefined, {
									MENUID : menu.id,
									MENUEVENT : menu.parameter
								}, menu.keepOpen);
							}
						}
					} else if (addin.type === 'WO') { // 组件类型为工单
						var ot = a.getObjectiveType(menu.otId);
						if (!ot)
							$F.alert('市场活动不存在：' + menu.otId);
						else {
							a.openAddinWOByOtId({
								addinId: addin.id,
								tabName: menu.name,
								otId: ot.id,
								menuId: menu.id,
								parameters: {
									MENUID: menu.id,
									MENUEVENT: menu.parameter
								}
							});
						}
					} else if (addin.type === 'WO3') { //传说中的工单叁
						//点击菜单时候，根据frameMapId来打开工单三
						let comment = menu.comment, customerGuid, stepId, readonly, alwaysNew, rolegroupId;
						if (comment) {
							try {
								comment = JSON.parse(comment);
								customerGuid = comment.customerGuid;
								stepId = comment.stepId;
								readonly = comment.readonly;
								rolegroupId = comment.rolegroupId;
								if (rolegroupId === '{C_GRP}') {
									rolegroupId = a.groupId;
								}
								alwaysNew = comment.alwaysNew;
							} catch(e) {}
						}
						if (a.customMenuParams["wo.rolegroupId"]) { // 从自定义参数中获取配置
							rolegroupId = a.customMenuParams["wo.rolegroupId"];
						}
						a.openAddinWOByOtId({
							addinId: addin.id,
							tabName: menu.name,
							otId: menu.otId,
							menuId: menu.id,
							parameters: {
								MENUID: menu.id,
								MENUEVENT: menu.parameter
							},
							wo3: true,
							customerGuid: customerGuid,
							stepId: stepId,
							dbPool: '',
							readonly: readonly,
							alwaysNew,
							rolegroupId
						});
					} else if (addin.type === 'DYN' || addin.type === 'DYN2') { // 组件类型为无状态动态页面
						let tabName = menu.name;
						let parameters = {
							MENUID : menu.id,
							MENUEVENT : menu.parameter
						};
						if (menu.parameter) {
							try {
								let param = JSON.parse(menu.parameter);
								if (param.tabName) {
									tabName = param.tabName;
								}
								parameters = Object.assign(parameters, param);
							} catch (e) {}
						}
						a.openAddinDyn(menu.otId, tabName, parameters, menu.keepOpen, 'DYN', addin.type === 'DYN2');
					} else if (addin.type === 'PLUGIN') { // 组件类型为插件
						let parameters = {
							MENUID : menu.id,
							MENUEVENT : menu.parameter
						};
						if (menu.parameter) {
							try {
								let param = JSON.parse(menu.parameter);
								parameters = Object.assign(parameters, param);
							} catch (e) {}
						}
						a.openAddinPlugin(menu.name, addin, menu.addinCmt, parameters, menu.keepOpen);
					} else {
						$F.err("不能识别的组件类型: " + addin.type);
					}
					if (menu.callNatureId) {
						a.events.notify($CONST.ModuleEvent.EXTERNAL_MESSAGE, {
							type : "setCallNature",
							callNatureId : menu.callNatureId
						});
					}
					hide();
				}, '处理事务中')
			}

			/**
			 * 打开VIEW
			 * @method openAddinView
			 * @param tabName {string} TAB标签
			 * @param addin {object} 组件对象
			 * @param url {string} 外部链接
			 * @param [options] {string} 另行开打浏览器窗口名称，改参数为空时代表在NGS的TAB内打开URL
			 * @param [specs] {string} 弹出窗口时候的相关参数，例如： scrollbars=yes,resizable=1,modal=false,alwaysRaised=yes
			 * @param [keepOpen] {boolean} 是否不允许关闭
			 */
			openAddinView (tabName, addin, url, options, specs, keepOpen) {
				var proj = this;
				if ($.type(options) === 'string') {
					options = {
						target: options
					}
				} else {
					options = options || {};
				}

				if (options.target) {
					window.open($F.addArgumentsToURL(url, proj), options.target, specs);
					return;
				}
				if (addin.multiple) {
					// 组件允许多实例
					const navs = proj.getNavs();
					for (let i = 0; i < navs.length; i++) {
						let $this = $(navs[i]);
						if ($this.data("addinId") == addin.id) {
							if ($this.data("url") == url) {
								// 相同URL已经打开，切换显示
								proj.tabs.xtabs({
									active : i
								});
								return;
							}
						}
					}
				} else {
					// 组件仅允许单实例
					const tabIdx = proj.tabFind(addin.id);
					if (tabIdx > -1) {
						// 组件已经被打开
						const tab = $(proj.getNavs()[tabIdx]);
						if (tab.data("url") == url) {
							// 相同URL已经打开，切换显示
							proj.tabs.xtabs({
								active : tabIdx
							});
							return;
						} else {
							proj.tabClose(tab);
						}
					}
				}

				// 创建新TAB，并加载组件
				const tab = this.tabCreate(Guid.raw(), tabName, !keepOpen);
				tab.data("container").append('<iframe  allow="camera;microphone" src="' + $F.addArgumentsToURL(url, proj) + '"></iframe>');
				tab.data("addinId", addin.id);
				tab.data("url", url);
				const instance = {
					id : tab.attr("data-id"),
					project : proj,
					type : 'URL'
				};
				tab.data("instance", instance);
				if (options.classes) {
					tab.addClass(options.classes)
				}
				instance.addinId = addin.id;
				$(".page-loading", tab.data("container")).remove();
			}

			/**
			 * 打开PLUGIN
			 * @method openAddinPlugin
			 * @param tabName {string} TAB标签
			 * @param addin {object} 组件对象
			 * @param [addinParam] {any} 组件参数
			 * @param [parameter] {any} 入参，JSON格式对象
			 * @param keepOpen {boolean} 是否不允许关闭
			 */
			openAddinPlugin (tabName, addin, addinParam, parameter, keepOpen) {
				// console.log('OpenPlugin(' + addinParam + '):', addin);
				const proj = this;
				if (addin.type === 'URL') {
					proj.openAddinView(tabName, addin, addinParam, parameter, keepOpen);
				} else {
					if (!$E.plugins[addin.id]) {
						require([ 'js/plugins/' + addin.notes + $E.jsv ], function(plugin) {
							$E.plugins[addin.id] = plugin;
							proj.openPlugin(tabName, $E.plugins[addin.id], addin, addinParam, parameter, keepOpen);
						})
					} else
						proj.openPlugin(tabName, $E.plugins[addin.id], addin, addinParam, parameter, keepOpen);
				}
			}

			openPlugin (tabName, plugin, addin, addinParam, parameter, keepOpen) {
				const proj = this,
					avaliable = plugin.avaliable(proj, addin, addinParam, parameter),
					navs = proj.getNavs();
				if (avaliable.code <= 0) {
					$F.alert('Plugin启动验证失败：' + avaliable.message);
					return;
				}
				if (addin.multiple) {
					// 组件允许多实例
					for (let i = 0; i < navs.length; i++) {
						let $this = $(navs[i]);
						if ($this.data("addinId") == addin.id) {
							if ($this.data("addinParam") == addinParam) {
								// 相同URL已经打开，切换显示
								proj.tabs.xtabs({
									active : i
								})
								$this.data("instance").active(parameter);
								return;
							}
						}
					}
				} else {
					// 组件仅允许单实例
					const tabIdx = proj.tabFind(addin.id);
					if (tabIdx > -1) {
						// 组件已经被打开
						const tab = $(navs[tabIdx]);
						if (tab.data("addinParam") == addinParam) {
							// 相同组件入参已经打开，切换显示
							proj.tabs.xtabs({
								active : tabIdx
							});
							return;
						} else {
							proj.tabClose(tab);
						}
					}
				}

				// 创建新TAB，并加载组件
				if (!tabName)
					tabName = addin.name;
				const tab = this.tabCreate(Guid.raw(), tabName, !keepOpen);
				tab.data("addinId", addin.id);
				tab.data("addinParam", addinParam);
				tab.data("parameter", parameter);
				const instance = plugin.newInstance(proj, tab.data("container"), addin, addinParam, parameter);
				instance.type = 'PLUGIN';
				tab.data("instance", instance);
				instance.addinId = addin.id;
				instance.isPlugin = true;
				$('.page-loading', tab.data("container")).remove();
			}

			openChatPlugin(container, parameters) {
				const proj = this;
				let instance = proj.getModuleInstance($CONST.ModuleName.CHAT);
				if (!instance) {
					if (proj.getParam('CHAECS') == '1') {
						//AgentState.EAS_READY : 2  如果当前电话是准备好状态
						if ($E.softphone && $E.softphone.currentAgentState == 2) {
							$F.alert('当前电话处于准备好状态，不能开启chat服务');
							return;
						}
					}
					$E.openChatPanel();
					const chatPlugin = $E.plugins[$CONST.ModuleName.CHAT];
					if (!chatPlugin) {
						require(['js/plugins/chat' + $E.jsv], function (plugin) {
							$E.plugins[$CONST.ModuleName.CHAT] = plugin;
							instance = plugin.newInstance(proj, container, parameters);
							proj.addModuleInstance($CONST.ModuleName.CHAT, instance);
						});
					} else {
						instance = chatPlugin.newInstance(proj, container, parameters);
						proj.addModuleInstance($CONST.ModuleName.CHAT, instance);
					}
				} else {
					$E.openChatPanel();
				}
			}

			openVideoPlugin(container) {
				const proj = this;

				if ($('#drawVideo', container).length === 0) {
					const drawVideo = $('<div class="videoChat"><style>#backgroundBox *, #drawVideo * {box-sizing: border-box;} .callcenter-tips-box.show {top: 70px;}</style><div id="drawVideo" style="display: flex;"><div id="videoLeft" style="width: 60%;height: 100%;display: flex;flex-direction: column;margin-bottom: 10px;"><div id="videoTop" style="height: 465px;"></div>  <div id="videoBottom" style="height: 50%;"></div></div><div id="videoRightTree" style="width: 40%;"></div></div></div>')
					drawVideo.appendTo(container)
					if($E.softphone) {
						setTimeout(function() {
							$E.softphone.core().drawVideo({
								videoWorkBench: "videoTop",
								busWorkBench: "videoBottom",
								treeWorkBench: "videoRightTree"
							});
						}, 100)
					}
				}
			}

			openChatMonitorPlugin(container) {
				const proj = this;

				if ($('#drawChatMonitor', container).length === 0) {
					const drawChatMonitor = $('<div class="chatMonitor" style="width: 100%; height: 100%; background: white;">' +
						'<div id="drawChatMonitor">' +
						'</div>' +
						'</div>')
					drawChatMonitor.appendTo(container)
				}
			}

			/**
			 * 在指定的容器内打开PLUGIN
			 * @method embAddinPlugin
			 * @param container {DOM} 容器DOM
			 * @param addin {object} 组件对象
			 * @param [addinParam] {any} 组件参数
			 * @param [parameter] {any} 插件，JSON格式对象
			 * @param [callback] {function} 回调
			 * @param [async] {boolean} 是否异步
			 */
			embAddinPlugin (container, addin, addinParam, parameter, callback, async) {
				const proj = this;
				if (!$E.plugins[addin.id]) {
					require([ 'js/plugins/' + addin.notes + $E.jsv ], function(plugin) {
						$E.plugins[addin.id] = plugin;
						proj.embPlugin(container, $E.plugins[addin.id], addin, addinParam, parameter, callback, async);
					})
				} else {
					proj.embPlugin(container, $E.plugins[addin.id], addin, addinParam, parameter, callback, async);
				}
			}

			embPlugin (container, plugin, addin, addinParam, parameter, callback, async) {
				const proj = this,
					avaliable = plugin.avaliable(proj, addin, addinParam, parameter);
				if (avaliable.code <= 0) {
					$F.alert(avaliable.message);
					$F.err('Project - openPlugin', 'PLUGIN启动验证失败, TOKEN: ' + proj.token);
					return;
				}
				let oldInstance = container.data('instance');
				if (oldInstance) {
					if (oldInstance.mounting) {
						return {};
					}
					oldInstance.destroy && oldInstance.destroy();
					container.removeData('instance');
					container.empty();
				}
				if (async) {
					const busyId = $F.busyShow();
					plugin.newInstance(proj, container, addin, addinParam, parameter, (instance) => {
						instance.type = 'PLUGIN';
						container.data('instance', instance);
						$F.busyHide(busyId);
						callback && callback(instance);
					});
				} else {
					const instance = plugin.newInstance(proj, container, addin, addinParam, parameter);
					instance.type = 'PLUGIN';
					container.data('instance', instance);
					callback && callback(instance);
					return instance;
				}
			}

			/**
			 * 指定容器中打开一个工单3
			 * @param container {object} 显示工单的容器
			 * @param woParams {object} 包括了：
			 * 		otId {string} 工单MAPID，如果是新建工单，需要入参工单ID
			 * 		oId {string} ObjectiveGuid, 打开一个已经存在的工单，需要入参ObjectiveGuid，此时woId可入参空
			 * 		taskId {string} 工单节点步骤ID, 打开一个已经存在的工单时有效
			 * 		[notifyId] {string} 工单提醒id
			 * 		[readonly] {boolean} 是否是只读打开，如果是只读打开就不会校验是否重复打开
			 * 		[rolegroupId] {string} 子流程id
			 * 		[forceProxy] {boolean} 是否强制代理
			 *		[tabc] {string} tabc表名
			 * @param [parameters] {object} 入参环境
			 */
			embWo3 (container, woParams, parameters) {
				// 创建工单instance于container中
				// 销毁主页面时，必须销毁该主页面中所有嵌套的工单instance
				var proj = this;
				if (woParams.oId) {
					let workorder = $W3.create(proj);
					let hasOpened = proj.isWOOpened({
						oId : woParams.oId,
						taskId : woParams.taskId,
						notifyId : woParams.notifyId
					});
					if (hasOpened.opened && !woParams.readonly) {
						$F.alert('不能重复打开工单: ' + JSON.stringify(woParams));
						return;
					}
					$F.log("Project - embWo3", woParams);
					return workorder.loadByObjectiveGuid({
						objectiveGuid: woParams.oId,
						taskId: woParams.taskId,
						addinId: 'AddinWO3',
						parameters: parameters,
						notifyId: woParams.notifyId,
						container: container,
						readonly: woParams.readonly,
						forceProxy: woParams.forceProxy,
						tabc: woParams.tabc
					});
				} else if (woParams.otId) { //根据map_id打开工单（可能是创建新工单）
					let objectiveType = proj.getFrameMap(woParams.otId);
					if (!objectiveType) {
						$F.alert('工单不存在：' + woParams.otId);
						return;
					}
					let dupFlag = objectiveType.multiple;
					let hasOpened = proj.isWOOpened({
						otId : woParams.otId,
						dupFlag : dupFlag,
						wo3 : true
					});
					if (hasOpened.opened) {
						$F.alert('不能重复打开工单: ' + woParams.otId);
						return;
					}
					let workorder = $W3.create(proj);
					$F.log("Project - embWo3", " otId: " + woParams.otId);
					return workorder.loadByOtId({
						mapId: woParams.otId,
						dupFlag: dupFlag,
						addinId: 'AddinWO3',
						parameters: parameters,
						container: container,
						customerGuid: woParams.customerGuid,
						stepId: woParams.stepId,
						readonly: woParams.readonly,
						rolegroupId: woParams.rolegroupId
					});
				}
			}

			/**
			 * 指定容器中打开一个动态页面
			 * @param container {object} 显示页面的容器，组件对象或者是组件名称
			 * @param dynId {string} 动态页面ID
			 * @param [parameters] {object} 入参环境
			 */
			embDyn (container, dynId, parameters) {
				// 创建instance于container中
				// 销毁主页面时，必须销毁该主页面中所有嵌套的instance
				var proj = this, instance = $D.createInstance(this, dynId, '', '', parameters, false, 'DYN', true);
				if (instance) {
					$D.live(instance, container);
				} else
					$F.err('Project - embDyn', '创建实例失败：' + dynId);
				return instance;
			}

			/**
			 * 在浮动层中打开一个动态页面作为模组使用
			 */
			layerDyn (runtime, dynId, options) {
				var dom = $('<div class="layerDyn" />'), instance = this.embDyn(dom, dynId, options.parameters);
				if (! instance)
					return;

				if (! $('.dyn_box', dom).css('backgroundColor'))
					$('.dyn_box', dom).css('backgroundColor', 'white')

				var maskOptions = {
					clickToHide : options.clickToHide,
					fixed : true,
					boxShadow : "1px 3px 10px rgba(128, 128, 128, 0.5)",
					onOpen : function() {
						var combox;
						$('select', dom).each(function() {
							combox = $(this).data('ctrl');
							combox && $.isFunction(combox.reset) && combox.reset();
						})
						$('.dyn_ckeditorInput', dom).each(function() {
							$(this).data('ckeditor').redraw();
						})
						instance.registerEvent('_MODULEINSTANCE_EVENT_SUBMIT', function(data) {
							Helper.masker.hide(dom);
							$.isFunction(options.callback) && options.callback(data, runtime);
						});
						instance.registerEvent('_MODULEINSTANCE_EVENT_CANCEL', function() {
							Helper.masker.hide(dom);
						});
					},
					onClose : function() {
						instance.destroy();
						dom.remove();
					}
				}
				if (options.inner) {
					maskOptions.container = runtime.instance.runtime.container;
					maskOptions.zIndex = 10001;
				}
				Helper.masker.show(dom, maskOptions);
			}

			dialogDyn (runtime, dynId, options) {
				var dom = $('<div class="layerDyn" />'), instance = this.embDyn(dom, dynId, options.parameters);
				if (! instance)
					return;

				var model = {
					title: options.title || '模组',
					icon: options.icon || 'cube',
					okTip: options.okTip,
					cancelTip: options.cancelTip,
					container: (options.inner ? runtime.instance.runtime.container : undefined),
					zIndex: (options.inner ? 10001: undefined),
					removable: true,
					escapable: options.escapable,
					submitAlways: options.submitAlways,
					onOpen: function() {
						var dialog = this, combox, submitBtn = $('.btnsBar>.js_submit', dialog);
						$('select', dom).each(function() {
							combox = $(this).data('ctrl');
							combox && $.isFunction(combox.reset) && combox.reset();
						})
						$('.dyn_ckeditorInput', dom).each(function() {
							$(this).data('ckeditor').redraw();
						})
						instance.registerEvent('_MODULEINSTANCE_EVENT_SUBMITAVAILABLE', function(available) {
							submitBtn.prop('disabled', !available);
						});
						instance.registerEvent('_MODULEINSTANCE_EVENT_SUBMIT', function(data) {
							$F.dialogClose(dialog);
							options.callback && options.callback(data, runtime);
						});
						instance.registerEvent('_MODULEINSTANCE_EVENT_CANCEL', function() {
							$F.dialogClose(dialog);
						});
						if (! options.submitAlways && ! instance.getAttribute('_MODULEINSTANCE_DIALOG_SUBMITALWAYS'))
							submitBtn.prop('disabled', true);

						var title = instance.getAttribute('_MODULEINSTANCE_DIALOG_TITLE');
						if (title)
							$('.exTitle>span', dialog).text(title);
						var icon = instance.getAttribute('_MODULEINSTANCE_DIALOG_ICON');
						if (icon)
							$('.topIcon', dialog).html('<i class="' + $F.fas(icon) + '"/>');
						var clazz = options.cssClass || instance.getAttribute('_MODULEINSTANCE_DIALOG_CLASS');
						if (clazz)
							dialog.addClass(clazz);
						if (options.cssStyles)
							$('.dialogContent', dialog).css(options.cssStyles);
						$('>form', dialog).attr('novalidate', true);
					},
					onSubmit: function(close) {
						var submitCallback = instance.getAttribute('_MODULEINSTANCE_EVENT_ONSUBMIT');
						if (submitCallback) {
							submitCallback(function(data) {
								close();
								$.isFunction(options.callback) && options.callback(data, runtime);
							})
						} else {
							close();
						}
					}
				};
				if (options.cancelable) {
					model.onCancel = function(close) {
						close();
					}
				}
				$F.createDialog(dom, model);
			}

			/**
			 * 根据业务数据，打开动态页面
			 * @method openAddinObjByOId
			 * @param addinId {string} 组件对象
			 * @param tabName {string} TAB标签
			 * @param oId {string} Objective对象ID，该参数仅当非菜单下打开时有效
			 * @param [parameters] {any} 入参，JSON格式对象
			 * @param [keepOpen] {boolean} 是否保持开启状态，不可关闭
			 */
			openAddinObjByOId (addinId, tabName, oId, parameters, keepOpen) {
				var proj = this,
					addin = proj.getAddin(addinId);
				if (addin) {
					// 根据oId创建objective对象
					$F.dataService('SystemDo2', {
						data : $F.sqlbean('DynBD.Select.Obj', proj.token, [ $E.staff.epidSql, '', oId ]),
						digest : proj.digest,
						success : function(data) {
							if (data.code > 0) {
								// console.log(data.value);
								if (data.value[0].count < 1)
									return $F.alert('用户数据不存在：' + oId);

								var otId = data.value[0].fields[0][1];
								var objectiveType = proj.getObjectiveType(otId);
								if (!objectiveType)
									return $F.alert('市场活动不存在：' + otId);
								proj.openAddinObj(tabName, addin, objectiveType, oId, parameters, keepOpen);
							}
						}
					})
				} else
					$F.alert('组件不存在：' + addinId);
			}

			/**
			 * 根据业务数据，打开动态页面
			 * @method openAddinObjByOtId
			 * @param addinId {string} 组件对象
			 * @param tabName {string} TAB标签
			 * @param otId {string} ObjectiveType对象ID
			 * @param parameters {any} 入参，JSON格式对象
			 * @param keepOpen {boolean} 是否保持开启状态，不可关闭
			 */
			openAddinObjByOtId (addinId, tabName, otId, parameters, keepOpen) {
				var proj = this,
					addin = proj.getAddin(addinId),
					objectiveType = proj.getObjectiveType(otId);
				if (!addin)
					return $F.alert('组件不存在：' + addinId);
				if (!objectiveType)
					return $F.alert('市场活动不存在：' + otId);

				//console.info(addin)
				proj.openAddinObj(tabName, addin, objectiveType, undefined, parameters, keepOpen);
			}

			/**
			 * 打开一个市场活动
			 * @method openAddinObj
			 * @param tabName {string} TAB标签
			 * @param addin {object} 组件对象
			 * @param ot {object | string} 市场活动对象 或者 市场活动类型id
			 * @param oId {string} Objective对象，该参数仅当非菜单下打开时有效
			 * @param [parameters] {any} 入参，JSON格式对象
			 * @param [keepOpen] {boolean} 是否保持开启状态，不可关闭，默认为false
			 * @return {instance} 动态页面实例对象
			 */
			openAddinObj (tabName, addin, ot, oId, parameters, keepOpen) {
				const proj = this, navs = proj.getNavs();
				if (typeof (addin) === 'string') {
					addin = proj.getAddin(addin);
				}
				if (typeof (ot) === 'string') {
					ot = proj.getObjectiveType(ot);
				}
				$F.log('Project - openAddinObj', '调用组件：' + addin.id);
				// 组件允许多实例
				for (let i = 0; i < navs.length; i++) {
					let $this = $(navs[i]);
					if ($this.data("addinId") == addin.id && $this.data("otId") == ot.id) {
						// 相同市场活动已经打开
						let instance = $this.data("instance");
						if (instance.stateful) {
							// (1)该市场活动实例为有保存状态（即有客户对象）
							if (instance.flag || instance.oId == oId) {
								// (2)并且 实例为新创建的objective 或者 与当前入参请求oId一致
								$this.show();
								proj.tabs.xtabs({
									active : i
								});
								return instance;
							}
						} else {
							if (!instance.multiple) {
								// (1) 无状态
								// (2) 当前实例不允许多副本（默认值）
								$this.show();
								proj.tabs.xtabs({
									active : i
								});
								return instance;
							}
						}
					}
				}

				const instance = $D.createInstance(proj, ot.addinCmtId, ot.id, oId, parameters, false, 'OBJ');
				if (instance) {
					// 创建新TAB，并加载组件
					if (!tabName)
						tabName = ot.name;
					const tab = this.tabCreate(Guid.raw(), tabName, !keepOpen);
					tab.data("addinId", addin.id);
					tab.data("otId", ot.id);
					tab.data("instance", instance);
					instance.tabCtrl = tab;
					instance.addinId = addin.id;
					$E.progressing = true;
					$D.live(instance, tab.data("container"));
					$E.progressing = false;
				} else
					$F.err('Project - openAddinObj', '创建实例失败：' + ot.addinCmtId);
			}

			/**
			 * 打开一个普通的无状态动态页面
			 * @param dynId {string} 动态页面ID
			 * @param tabName {string} 标签名称，为空时取动态页面默认名称
			 * @param [parameters] {any} 入参，JSON格式对象
			 * @param [keepOpen] {boolean} 是否为不可关闭
			 * @param [type] {string} 动态页面类型，动态页面='DYN'，工单='WO'，客户资料='CUST'。默认为'DYN'
			 * @param [veSupport] {boolean} 是否VE类型动态页面，默认为false
			 * @return {instance}
			 */
			openAddinDyn (dynId, tabName, parameters, keepOpen, type, veSupport) {
				const proj = this;
				let addinId = (veSupport ? 'AddinDYN' : 'AddinBD');
				if (dynId[0] == '!') {
					dynId = dynId.substr(1);
					addinId = 'AddinDYN';
					veSupport = true;
				}
				if (parameters == undefined)
					parameters = {};

				if (!dynId) {
					$F.err('Project - openAddinDyn', '无效的动态页面ID');
					return null;
				}

				const navs = proj.getNavs();
				for (let i = 0; i < navs.length; i++) {
					let tab = $(navs[i]);
					if (tab.data("addinId") == addinId) {
						let instance = tab.data("instance");
						if (!instance)
							$F.err('Project - openAddinDyn', '在标签中没有找到有效的实例', tab);
						else {
							if (instance.dynId == dynId) {
								if (!instance.ownerInclusive) {
									if (instance.owner !== parameters.OWNER)
										// 非独占，且不同归属，销毁并打开新页面
										proj.tabClose(tab);
									else {
										// 非独占，但是相同归属人，切换
										tab.show();
										proj.tabs.xtabs({
											active : i
										});
										return instance.active(parameters);
									}
								} else {
									if (instance.owner == parameters.OWNER) {
										// 独占，相同归属，切换
										tab.show();
										proj.tabs.xtabs({
											active : i
										});
										return instance.active(parameters);
									} else {
										// 不做任何处理，打开新页面
									}
								}
							}
						}
					}
				}

				const instance = $D.createInstance(this, dynId, '', '', parameters, false, type, veSupport);
				if (instance) {
					//if (!instance.stateful) {
					// 创建新TAB，并加载组件
					const tab = this.tabCreate(Guid.raw(), tabName || instance.struct.name, !keepOpen);
					tab.data("addinId", addinId);
					tab.data("instance", instance);
					instance.tabCtrl = tab;
					instance.addinId = addinId;
					$E.progressing = true;
					$D.live(instance, tab.data("container"));
					$E.progressing = false;
					//} else
					//	$F.alert('有状态动态页面不允许直接打开');
				} else
					$F.err('Project - openAddinDyn', '创建实例失败：' + dynId);
			}

			/**
			 * 打开一个组件页面用于测试
			 * @param dynId {string} 动态页面ID
			 * @param [parameters] {any} 入参，JSON格式对象
			 */
			testCp (dynId, parameters) {
				parameters = parameters || {};
				const proj = this, navs = proj.getNavs(), addinId = 'CP-TEST';
				for (let i = 0; i < navs.length; i++) {
					let tab = $(navs[i]);
					if (tab.data("addinId") == addinId) {
						let instance = tab.data("instance");
						if (!instance)
							$F.err('Project - openAddinDyn', '在标签中没有找到有效的实例', tab);
						else {
							if (instance.dynId == dynId) {
								tab.show();
								proj.tabs.xtabs({
									active : i
								});
								return instance.active(parameters);
							}
						}
					}
				}

				const instance = $D.createInstance(this, dynId, '', '', parameters, false, 'DYN', true);
				if (instance) {
					// 创建新TAB，并加载组件
					const tab = this.tabCreate(Guid.raw(), 'CP-' + instance.struct.name, true);
					tab.data("addinId", addinId);
					tab.data("instance", instance);
					instance.tabCtrl = tab;
					instance.addinId = addinId;
					instance.struct.cp = true;
					$E.progressing = true;
					$D.live(instance, tab.data("container"));
					$E.progressing = false;
				} else
					$F.err('Project - testCp', '创建实例失败：' + dynId);
			}

			/**
			 * 判断一个工单是不是已经打开了
			 * @param params
			 */
			isWOOpened (params) {
				const proj = this, navs = proj.getNavs();
				if (params.otId) {
					//otId方式打开的时候需要到工单内部获取结构后才能判断
				} else if (params.oId) {
					const oId = params.oId,
						taskId = params.taskId,
						notifyId = params.notifyId;
					for (let i = 0; i < navs.length; i++) {
						let $this = $(navs[i]), instance = $this.data("instance");
						if (instance.stateful && instance.oId === oId && $this.data("taskId") === taskId && $this.data("notifyId") === notifyId) {
							// (1)该市场活动实例为有保存状态（即有客户对象）
							// (2)并且与当前入参请求oId一致（通过oId打开的工单，肯定不是新建工单，所以直接判断oId是否相同）
							// (3)并且taskId也一致，多任务模式中，oId相同taskId不同的时候也可以打开多个
							// (4)当oId和taskId都相同时候，如果notifyId不同，也可以同时打开
							return {
								opened : true,
								index : i,
								instance : instance
							};
						}
						if (instance.wos) {
							for (let j = 0; j < instance.wos.length; j++) {
								let wo = instance.wos[j];
								if (wo.guid === oId) {
									return {
										opened : true,
										index : i,
										instance : instance
									};
								}
							}
						}
					}
				}
				return {
					opened : false
				};
			}

			// fixed by kevin, appending 'parameters' for menu event
			openAddinWOByOtId (addinId, tabName, otId, menuId, parameters, wo3, customerGuid, stepId, dbPool, readonly, alwaysNew, rolegroupId) {
				if (typeof addinId === "object") {
					let params = addinId;
					addinId = params.addinId;
					tabName = params.tabName;
					otId = params.otId;
					menuId = params.menuId;
					parameters = params.parameters;
					wo3 = params.wo3;
					customerGuid = params.customerGuid;
					stepId = params.stepId;
					dbPool = params.dbPool;
					readonly = params.readonly;
					alwaysNew = params.alwaysNew;
					rolegroupId = params.rolegroupId;
				}
				const proj = this, addin = proj.getAddin(addinId);
				let objectiveType;
				if (wo3) { //如果是工单三模式，就是获取WO_FrameMap
					objectiveType = proj.getFrameMap(otId);
				} else {
					objectiveType = proj.getObjectiveType(otId);
				}
				if (!objectiveType) {
					return $F.alert('工单不存在：' + otId);
				}
				let dupFlag = objectiveType.multiple; //根据objectivetype的duplicate判断是不是要新增objective
				let hasOpened = proj.isWOOpened({
					otId : otId,
					dupFlag : dupFlag,
					wo3 : wo3,
					customerGuid : customerGuid
				});
				if (!addin)
					return $F.alert('组件不存在：' + addinId);
				$F.log('Project - openAddinWOByOtId', '调用多实例组件：' + addin.id);
				if (hasOpened.opened) {
					if (hasOpened.instance.struct.version !== 'DYN2') { //新版本动态页面没有-99了，都在active命令里了
						hasOpened.instance.doCmdIncludeComponents("P-99");
					}
					proj.tabs.xtabs({
						active : hasOpened.index
					});
					return;
				}
				let workorder = wo3 ? $W3.create(proj, addin.id, dbPool) : $W.create(proj, addin.id);
				$F.log("Project.openAddinWOByOtId", "AddinId: " + addin.id + "  tabName: " + tabName + "   MenuId: " + menuId +
					" otId: " + otId + "  dupFlag: " + dupFlag + ' wo3: ' + wo3 + ' customerGuid:' + customerGuid + ' stepId:' + stepId +
					" readonly: " + readonly + " alwaysNew: " + alwaysNew + " rolegroupId: " + rolegroupId);
				return workorder.loadByOtId({
					mapId: otId,
					dupFlag: dupFlag,
					addinId: addinId,
					tabName: tabName,
					parameters: parameters,
					customerGuid: customerGuid,
					stepId: stepId,
					readonly: readonly,
					alwaysNew: alwaysNew,
					rolegroupId: rolegroupId
				});
			}

			openAddinWOByOId (addinId, tabName, oId, parameters) {
				const proj = this, addin = proj.getAddin(addinId), navs = proj.getNavs();
				if (!addin)
					return $F.alert('组件不存在：' + addinId);
				if (addin.multiple) {
					$F.log('Project - openAddinWOByOId', '调用多实例组件：' + addin.id);
					for (let i = 0; i < navs.length; i++) {
						let instance = $(navs[i]).data("instance");
						if (instance.stateful && instance.oId == oId) {
							// (1)该市场活动实例为有保存状态（即有客户对象）
							// (2)并且与当前入参请求oId一致（通过oId打开的工单，肯定不是新建工单，所以直接判断oId是否相同）
							proj.tabs.xtabs({
								active : i
							});
							return;
						}
					}
				} else {
					$F.log('Project - openAddinWOByOId', '调用单实例组件：' + addin.id);
					const tabIdx = proj.tabFind(addin.id);
					if (tabIdx > -1) {
						// 组件已经被打开
						const tab = $(navs[tabIdx]), instance = tab.data("instance");
						if (instance.stateful && (instance.flag || instance.oId == oId)) {
							// (1)该市场活动实例为有保存状态（即有客户对象）
							// (2)并且 实例为新创建的objective 或者 与当前入参请求oId一致
							proj.tabs.xtabs({
								active : tabIdx
							});
							return;
						} else {
							proj.tabClose(tab);
						}
					}
				}
				$W.create(proj, addinId).loadByObjectiveGuid(oId, addinId, tabName, parameters);
			}

			/**
			 * 根据objective_guid和elitetask_guid打开工单3
			 * @returns {*}
			 */
			openAddinWO3ByOId (addinId, tabName, oId, taskId, parameters, notifyId, dbPool, readonly, continuable, lastInstance, forceProxy, tabc) {
				if (typeof addinId === "object") {
					let params = addinId;
					addinId = params.addinId;
					tabName = params.tabName;
					oId = params.oId;
					taskId = params.taskId;
					parameters = params.parameters;
					notifyId = params.notifyId;
					dbPool = params.dbPool;
					readonly = params.readonly;
					continuable = params.continuable;
					lastInstance = params.lastInstance;
					forceProxy = params.forceProxy;
					tabc = params.tabc;
				}
				const proj = this, addin = proj.getAddin(addinId);
				if (!addin)
					return $F.alert('组件不存在：' + addinId);
				const hasOpened = proj.isWOOpened({
					oId : oId,
					taskId : taskId,
					notifyId : notifyId
				});
				if (hasOpened.opened) {
					// (1)该市场活动实例为有保存状态（即有客户对象）
					// (2)并且与当前入参请求oId一致（通过oId打开的工单，肯定不是新建工单，所以直接判断oId是否相同）
					// (3)并且taskId也一致，多任务模式中，oId相同taskId不同的时候也可以打开多个
					// (4)当oId和taskId都相同时候，如果notifyId不同，也可以同时打开
					proj.tabs.xtabs({
						active : hasOpened.index
					});
					return;
				}
				const workorder = $W3.create(proj, addinId, dbPool);
				return workorder.loadByObjectiveGuid({
					objectiveGuid: oId,
					taskId: taskId,
					addinId: addinId,
					tabName: tabName,
					parameters: parameters,
					notifyId: notifyId,
					readonly: readonly,
					continuable: continuable,
					lastInstance: lastInstance,
					forceProxy: forceProxy,
					tabc: tabc
				});
			}

			openAddinWOByStepId (addinId, tabName, subId, parameters) {
				const proj = this, addin = proj.getAddin(addinId), navs = proj.getNavs();
				let hasOpened = false;
				if (!addin) {
					return $F.alert('组件不存在：' + addinId);
				}
				if (addin.multiple) {
					$F.log('Project - openAddinWOByStepId', '调用多实例组件：' + addin.id);
					for (let i = 0; i < navs.length; i++) {
						let instanceSubId = $(navs[i]).data("subId");
						if (instanceSubId == subId) {
							hasOpened = true;
							break;
						}
					}
					if (hasOpened) {
						proj.tabs.xtabs({
							active : i
						});
						return;
					}
				} else {
					$F.log('Project - openAddinWOByStepId', '调用单实例组件：' + addin.id);
					const tabIdx = proj.tabFind(addin.id);
					if (tabIdx > -1) {
						// 组件已经被打开
						let tab = $(navs[tabIdx]);
						let instanceSubId = tab.data("subId");
						if (instanceSubId == subId) {
							hasOpened = true;
						}
						if (hasOpened) {
							proj.tabs.xtabs({
								active : tabIdx
							});
							return;
						} else { //单实例情况下，找到同样的，就关掉老的
							proj.tabClose(tab);
						}
					}
				}
				$W.create(proj, addinId).loadBySubStep(subId, addinId, tabName, parameters);
			}

			/**
			 * 根据taskId去打开工单3
			 * @param taskId [string]
			 * @param updateToSelf [boolean] 是否要更新这个task的处理组处理人为当前登录组和人
			 * @param parameters [object] 入参
			 * @param dbPool [string]
			 * @param readonly [boolean] 是否只读
			 * @param [forceProxy] {boolean} 是否强制代理
			 */
			openAddinWO3ByTaskId (taskId, updateToSelf, parameters, dbPool, readonly, forceProxy) {
				const proj = this, ret = proj.commonQuerySync("ECore.Workorder.selectEliteTask", [ taskId ]);
				if (!$F.isNull(ret) && ret.value[0].count > 0) {
					if (updateToSelf) {
						const taskBoundData = new BoundData("elitetask" + $E.staff.epidSql);
						taskBoundData.KeyInfoEdit("elitetask_guid");
						taskBoundData.NewValueEdit("elitetask_guid", taskId);
						taskBoundData.NewValueEdit("handleby_id", $E.staff.id);
						taskBoundData.NewValueEdit("handlegroup_id", proj.groupId);
						$F.ajaxSync("SystemDo5", {
							token : proj.token,
							dbPool : dbPool || '',
							xml : taskBoundData.getData()
						}, proj.digest);
						$F.log('Project - openAddinWO3ByTaskId', 'Update EliteTask [' + taskId + '] to ' + $E.staff.id + ' and ' + proj.groupId);
					}

					const task = $F.makeJsonBean(ret.value[0]),
						oId = task.OBJECTIVE_GUID,
						mapId = task.MAP_ID,
						customerGuid = task.CUSTOMER_GUID;
					if (customerGuid) {
						proj.setCurrentCustomerByGuid(customerGuid);
						const frame = proj.getFrameMap(mapId);
						if (frame) {
							$F.log('Project - openAddinWO3ByTaskId', 'Open AddinWO3 - oId:' + oId + '  taskId: ' + taskId);
							proj.openAddinWO3ByOId({
								addinId: 'AddinWO3',
								tabName: frame.name,
								oId,
								taskId,
								parameters,
								dbPool,
								readonly,
								forceProxy
							});
						} else {
							$F.log('Project - openAddinWO3ByTaskId', 'No frame found by frameMapId:' + mapId);
						}
					} else {
						$F.log('Project - openAddinWO3ByTaskId', 'No customer found on task: ' + taskId);
					}
				} else {
					$F.log('Project - openAddinWO3ByTaskId', 'No elitetask found by guid: ' + taskId);
				}
			}

			/**
			 * 根据组件ID和市场活动ID弹出相应的TAB， 有保存状态的实例必须匹配oId
			 * @param addinId {string} 组件ID
			 * @param otId {string} 市场活动ID
			 * @param [oId] {string} ObjectiveGuid
			 * @return {boolean} 弹出成功为true，否则为false
			 */
			openTab (addinId, otId, oId) {
				const proj = this, navs = proj.getNavs();
				for (let i = 0; i < navs.length; i++) {
					let $this = $(navs[i]);
					if ($this.data("addinId") == addin.id) {
						// 相同组件已经打开
						if ($this.data("otId") == otId) {
							// 相同市场活动已经打开
							let instance = $this.data("instance");
							if (instance.stateful) {
								// 有保存状态的实例，必须匹配ObjectiveGuid
								if (instance.oId == oId) {
									// 该市场活动实例为有保存状态（即有客户对象）
									// 并且与当前入参请求oId一致，切换显示
									proj.tabs.xtabs({
										active : i
									})
									return true;
								}
							} else {
								proj.tabs.xtabs({
									active : i
								})
								return true;
							}
						}
					}
				}
				return false;
			}

			/**
			 * 把TAB实例添加到全局的缓存中
			 * @method addModuleInstance
			 * @param moduleName {string} 组件名称
			 * @param instance {object} TAB实例
			 */
			addModuleInstance (moduleName, instance) {
				if (!$F.isNull(instance)) {
					let instanceMap = this.moduleInstanceMap[moduleName];
					if (instanceMap == null || instanceMap == undefined) {
						instanceMap = {};
						this.moduleInstanceMap[moduleName] = instanceMap;
					}
					instanceMap[instance.id] = instance;
				}
			}

			/**
			 * 从缓存中查询某个TAB实例
			 * @method getModuleInstance
			 * @param moduleName {string} 组件名称
			 * @param instanceId {string} TAB实例ID
			 */
			getModuleInstance (moduleName, instanceId) {
				let instanceMap = this.moduleInstanceMap[moduleName];
				if ($F.isNull(instanceMap)) {
					return null;
				} else {
					if (instanceId) {
						return instanceMap[instanceId];
					} else {
						for (instanceId in instanceMap) {
							return instanceMap[instanceId]; //当不传递instanceId时候，返回第一个实例
						}
					}
				}
			}

			/**
			 * 从缓存中删除某个TAB实例
			 * @method removeModuleInstance
			 * @param moduleName {string} 组件名称
			 * @param instanceId {string} TAB实例ID
			 */
			removeModuleInstance (moduleName, instanceId) {
				let instanceMap = this.moduleInstanceMap[moduleName];
				if (!$F.isNull(instanceMap)) {
					let newMap = {};
					if (instanceId) {
						for (var id in instanceMap) {
							if (id != instanceId) {
								newMap[id] = instanceMap[id];
							}
						}
					}
					this.moduleInstanceMap[moduleName] = newMap;
				}
			}

			setCurrentCustomer (customer) {
				const proj = this;
				let needFireCustomerChange = false;
				proj.settingCustomer = true;
				$F.log("Project.setCurrentCustomer", "Call setCurrentCustomer: " + JSON.stringify(customer));

				if (customer == null) {
					if (proj.customer.getValue('customer_guid')) {
						needFireCustomerChange = true;
					}
					proj.customer.clear();
				} else {
					if (customer[0] != proj.customer.getValue('customer_guid')) {
						needFireCustomerChange = true;
					}
					for (var i = 0, j = customer.length; i < j; i++) {
						proj.customer.setPropertyByName(proj.customerFieldsCache.fields[i], customer[i]);
						proj.customer.setOriginalPropertyByName(proj.customerFieldsCache.fields[i], customer[i]);
					}

					//来电弹屏，选择客户后，workstart
					if (!proj.workDuration && proj.cwInfo.callStart &&
						(proj.cwInfo.callType == CallType.INBOUND || proj.cwInfo.callType == CallType.SIMULAT_INBOUND || proj.cwInfo.callType == CallType.PDS_OUTBOUND)) {
						// 在准备要工作开始之前，先判断一下当前是不是已经准备好状态，并且不再电话中，如果是则设置成未准备好
						// 当来电弹屏后，有种情况：坐席未选择客户，这时候挂断了电话，并切换了状态为准备好，这时候再去设置工作开始那就可能有问题
						// jeff来操作了，不需要了
						// if ($E.softphone.isAgentReady() && !$E.softphone.isCallBusy()) {
						// 	$E.softphone.notReady();
						// }
						proj.events.notify($CONST.ModuleEvent.ECM_WORKSTART, {
							workStatus : WorkInfo.WT_IB
						});
						$F.log("Project.setCurrentCustomer", "ECM_WORKSTART with customer " + proj.customer.getValue("customer_guid"));
						proj.cwInfo.callStart = false;
						$E.frameUnLock();
					}
				}
				//当当前客户guid与传入的customer的guid不同时候，才去触发customer改变
				if (needFireCustomerChange) {
					proj.events.notify($CONST.ModuleEvent.ECM_CUSTOMERCHANGE);
				}
				proj.settingCustomer = false;
			}

			/**
			 *
			 * @param customerGuid
			 * @param opts
			 * @returns
			 */
			setCurrentCustomerByGuid (customerGuid, opts) {
				const proj = this;
				let async = false, deferred, result = false;
				if (opts && opts.async == true) {
					async = true;
					deferred = $.Deferred();
				}

				if ($F.isNull(customerGuid) || $.trim(customerGuid) == "") {
					$F.log("Project - setCurrentCustomerByGuid", "clear customer");
					proj.setCurrentCustomer(null);
					return true;
				} else {
					$F.dataService('SystemDo2', {
						data : $F.sqlbean('ECore.Customer.searchByGuid', proj.token, [ $E.staff.epidSql, customerGuid ]),
						digest : proj.digest,
						async : async,
						success : function(data) {
							if (data.code < 1) {
								$F.alert(data.message);
								return deferred && deferred.reject();
							}
							if (data.value[0].count > 0) {
								proj.setCurrentCustomer(data.value[0].fields[0]);
								deferred && deferred.resolve();
								result = true;
							} else {
								deferred && deferred.reject();
							}
						},
						fail : function() {
							deferred && deferred.reject();
						}
					});
				}
				return async ? deferred.promise() : result;
			}

			openCustomer() {
				const proj = this;
				// 打开默认客户资料页面
				if (proj.getParam("OCASUM") !== "0") {
					proj.openCustomerTab();
					if ($E.query !== 'crm') {
						// 初始化外拨和小结模块
						require([ 'js/callsummary' + $E.jsv,
							'js/outbound' + $E.jsv ], function(CallSummary, Outbound) {
							proj.outbound = Outbound.getInstance(proj);
							proj.obUtil = Outbound.getOBUtil(proj);
							proj.callSummary = CallSummary.getInstance(proj);
							proj.callSummary.init();
							proj.outbound.init();
						});
					}
				}
			}

			/**
			 * 初始化开机界面
			 * @method openStartUpMenus
			 */
			openStartUpMenus () {
				const proj = this;

				// fixed by kevin
				// 2017/03/09
				// 框架mode入参调整，增加更多模式
				if ($E.mode == 3 || $E.mode == 31)
					return;
				loopMenus(this.menus, function(menu) {
					if (menu.startup) {
						proj.onMenu(menu);
					}
				})
			}

			findMenuById (menuId) {
				let ret;
				loopMenus(this.menus, function(menu) {
					// console.log(menu.name + " : " + menu.comment);
					if (menu.id == menuId) {
						ret = menu;
					}
				});
				return ret;
			}

			/**
			 * 打开客户资料页面
			 * @param custDynId 客户资料页面的动态页面id，不传递时候就用SBCUST
			 */
			openCustomerTab (custDynId) {
				const proj = this;
				if ($E.mode == 2 || $E.mode == 3 || $E.mode == 21 || $E.mode == 31)
					return;

				// 从缓存中获取已经打开的instance，如果找到则直接reopen
				let instance = proj.getModuleInstance($CONST.ModuleName.ADDIN_CUSTOMER);
				if (!$F.isNull(instance)) {
					proj.tabReopen(instance);
					return;
				}

				const sbcust = custDynId || proj.getParam($E.mo ? 'MOCUST' : 'SBCUST');
				if (!sbcust) {
					return;
				}

				// 客户资料作为一个独立的模块。在打开动态页面时获得instance实例对象，并对实例进行特殊操作
				instance = $D.createInstance(proj, sbcust, '', '', undefined, true, 'CUST', proj.veSupport);
				if (instance) {
					// 创建默认客户资料标签
					const tab = this.tabCreate('default', $F.lang('customer.customerinfo'), false);
					tab.data("instance", instance);
					tab.data('addinId', $CONST.ModuleName.ADDIN_CUSTOMER);
					$E.progressing = true;
					$D.live(instance, tab.data("container"));
					$E.progressing = false;
					proj.addModuleInstance($CONST.ModuleName.ADDIN_CUSTOMER, instance);

					proj.events.register($CONST.CTIEvent.CTI_ONCALLSTART, 'CustomerObject', function(callInfo) {
						$F.log("Customer.CTI_ONCALLSTART", "Receive CTI_ONCALLSTART with event_guid " + callInfo.getValue("event_guid"));
						const callType = callInfo.getValue("callType");
						if (callType == CallType.INBOUND) {
							let fireInboudCallstart = false;
							let param = {
								EVENT_GUID : callInfo.getValue("event_guid"),
								ANI : callInfo.getValue("ANI"),
								DNIS : callInfo.getValue("DNIS"),
								CALLTYPE : callType
							};

							$F.log("Davco Staff.workDuration...", proj.workDuration);
                            proj.workDuration = false;
							if (!proj.workDuration) {
								//弹屏后加锁
								$F.log("Customer.CTI_ONCALLSTART", "Lock frame when inbound popup customer module.");
								$E.frameLock({
									withoutHeader : true
								});
								proj.setCurrentCustomer(null);
								fireInboudCallstart = true;
							} else {
								//如果inbound后，从随路数据中获取到了customer_guid并且成功set了customer，而且进入工作了，这时候也需要发出80和81
								if (proj.cwInfo.setCustomerFromAttachData) {
									proj.cwInfo.setCustomerFromAttachData = false;
									fireInboudCallstart = true;
									param.CUSTFROMATTACHDATA = true;
								}
							}
							if (fireInboudCallstart) {
								if (instance.struct.version == 'DYN2') {
									instance.doCmdIncludeComponents('cti.inboundcallstart', param, instance.runtime, 'CustomerObject', true);
								} else {
									instance.doCmdIncludeComponents("P80", param);
									instance.doCmdIncludeComponents("P81", param);
								}
							}
						}
					});

					proj.events.register($CONST.CTIEvent.CTI_ONCALLESTABLISH, 'CustomerObject', function() {
						if (instance.struct.version === 'DYN2') {
							// 重复触发
							// instance.doCmdIncludeComponents('cti.callestablish', {}, instance.runtime, 'CustomerObject', true);
						} else {
							instance.doCmdIncludeComponents("P82");
							instance.doCmdIncludeComponents("P994");
						}
					});

					proj.events.register($CONST.CTIEvent.CTI_ONCALLEND, 'CustomerObject', function() {
						if (instance.struct.version === 'DYN2') {
							// 重复触发
							// instance.doCmdIncludeComponents('cti.callend', {}, instance.runtime, 'CustomerObject', true);
						} else {
							instance.doCmdIncludeComponents("P83");
							instance.doCmdIncludeComponents("P995");
						}
					});

					proj.events.register($CONST.ModuleEvent.ECM_WORKSTART, 'CustomerObject', function() {
						$F.log("Customer.ECM_WORKSTART", "Receive ECM_WORKSTART.");
						if (instance.struct.version === 'DYN2') {
							instance.doCmdIncludeComponents('work.start', {}, instance.runtime, 'CustomerObject', true);
						} else {
							instance.doCmdIncludeComponents("P98");
						}
					});

					proj.events.register($CONST.ModuleEvent.ECM_WORKEND, 'CustomerObject', function() {
						$F.log("Customer.ECM_WORKEND", "Receive ECM_WORKEND.");
						if (instance.struct.version === 'DYN2') {
							instance.doCmdIncludeComponents('work.end', {}, instance.runtime, 'CustomerObject', true);
						} else {
							instance.doCmdIncludeComponents("P99");
						}
					});

					instance.propertyListener('CUSTOMERMODULE', function(pName, pValue) {
						pName = pName.toUpperCase();
						if (pName === "TEMP.SEARCH") {
							if (pValue == "1") {
								proj.customer.query();
								$F.fireValue(instance, "TEMP.SEARCH", "0");
							}
						} else if (pName === "TEMP.CLEAR") {
							if (pValue == "1") {
								proj.customer.clear();
								proj.showCustomerDetail();
								$F.fireValue(instance, "TEMP.CLEAR", "0");
							}
						} else if (pName === "TEMP.SAVE") {
							if (pValue === "1") {
								proj.customer.save(function(customerGuid) {
									if (proj.customer.getProperty("customer_guid") !== customerGuid) {
										proj.setCurrentCustomerByGuid(customerGuid);
									}
								}, null, true);
								$F.fireValue(instance, "TEMP.SAVE", "0");
							}
						} else if (pName === "TEMP.CUST_CUSTOMER_GUID") {
							// 避免setCurrentCustomer后，触发的客户资料值改变，然后重复的去触发了一次setCurrentCustomerByGuid
							if (proj.settingCustomer) {
								return;
							}
							if (pValue.length < 36) {
								const c = 36 - pValue.length;
								for (let i = 0; i < c; i++) {
									pValue += " ";
								}
							}
							proj.setCurrentCustomerByGuid(pValue);
						}
					})
				} else
					$F.err('Project - openCustomerTab', '创建实例失败：' + sbcust);
			}

			/**
			 * 获得当前客户的GUID
			 * @method getCustomerGuid
			 * @return {string}
			 */
			getCustomerGuid () {
				return this.customer.getValue("customer_guid");
			}

			/**
			 * 返回当前拨打的客户的guid
			 * @returns {string}
			 */
			getCurrentCalledCustomerGuid () {
				return this.getCustomerGuid();
			}

			/**
			 * 返回当前联系客户的guid
			 * @returns {string}
			 */
			getCurrentContactCustomerGuid () {
				return this.contactCustomer.getValue("customer_guid") || '';
			}

			setCurrentContactCustomerGuid (ccg) {
				this.contactCustomer.setValue("customer_guid", ccg);
			}

			/**
			 * 清空当前客户
			 */
			clearCustomer () {
				var proj = this;
				$F.log("Project - clearCustomer", "clear customer");
				proj.customer.clear();
				proj.showCustomerDetail();
			}

			saveCustomer () {
				var proj = this;
				return new Promise(function(resolve, reject){
					proj.customer.save(function(customerGuid) {
						if (proj.customer.getProperty("customer_guid") !== customerGuid) {
							proj.setCurrentCustomerByGuid(customerGuid);
						}
						resolve();
					}, function(){
						reject();
					}, null, true);
				})
			}

			/**
			 * 展示当前客户到客户资料
			 * @method showCustomerDetail
			 */
			showCustomerDetail (values) {
				//获取客户资料实例
				const instance = this.getModuleInstance($CONST.ModuleName.ADDIN_CUSTOMER);
				if (instance) {
					for (let fieldName in this.customer.values) {
						values = values || this.customer.values;
						// fixed by kevin
						// (1) 有些用户数据表字段在UI上并未引用配置，因此需要做过滤，以防止出现MISS PROPERTY
						// (2) 直接引用instance.fireValue
						const propertyName = "temp.cust_" + fieldName;
						if (instance.getProperty(propertyName)) {
							instance.fireValue(propertyName, instance.runtime, {
								type : 0,
								data : this.customer.getProperty(fieldName),
								unbind: true
							});
						}
					}
				}
			}

			/**
			 * 修改当前客户的属性,同时通知ui
			 * @method changeCurrentCustomerProperty
			 * @param {string}fieldName
			 * @param {string}propertyValue
			 * @return {string}
			 */
			changeCurrentCustomerProperty (fieldName, propertyValue) {
				if (fieldName) {
					this.customer.setValue(fieldName, propertyValue);
					const instance = this.getModuleInstance($CONST.ModuleName.ADDIN_CUSTOMER);
					if (instance) {
						const propertyName = "temp.cust_" + fieldName;
						if (instance.getProperty(propertyName)) {
							instance.fireValue(propertyName, instance.runtime, {
								type : 0,
								data : propertyValue,
								unbind: true
							});
						}
					}
				}
			}

			/**
			 * 初始化工单查询画面，仅用户独立运行的WO模式
			 * @method openWoSearchTab
			 */
			openWoSearchTab () {
				const proj = this;
				let wossrh = proj.getParam('WOSSRH');
				if (!wossrh) wossrh = proj.getParam('JBDEB');
				if (!wossrh) {
					$F.alert($F.lang('sys.paramerr') + ': WOSSRH or JBDEB');
					return;
				}
				const instance = $D.createInstance(proj, wossrh, '', '', undefined, true, 'WO', proj.veSupport);
				if (instance) {
					// 创建默认工单查询标签
					const tab = this.tabCreate('defaultWossrh', '工单查询', false);
					tab.data("instance", instance);
					$E.progressing = true;
					$D.live(instance, tab.data("container"));
					$E.progressing = false;
					proj.addModuleInstance($CONST.ModuleName.ADDIN_WORKORDERSERCH, instance);
				} else
					$F.err('Project - openWoSearchTab', '创建实例失败：' + wossrh);
			}

			openStaticDyn () {
				const proj = this,
					staticDyn = proj.getParam('PUBZOO'),
					rarea = $('#mainBody .staticarea'),
					larea = $('#sideBar .staticarea');
				// empty and destroy exists dyn page
				let instance = larea.data('instance');
				if (instance) {
					instance.destroy();
					larea.empty().html('').hide();
				}
				instance = rarea.data('instance');
				if (instance) {
					instance.destroy();
					rarea.empty().html('').hide();
				}
				// searching current project static dyn page
				if (staticDyn) {
					let cs = staticDyn.toUpperCase().split('||'), height = 300, rs = 0, veSupport = proj.veSupport;
					if (cs.length > 1)
						height = parseInt(cs[1]);
					if (cs.length > 2)
						rs = parseInt(cs[2]);
					if (cs.length > 3)
						veSupport = parseInt(cs[3]) == 1;

					let area = rs ? rarea : larea;
					area.css('height', height+'px');
					area.show();
					instance = $D.createInstance(this, cs[0], '', '', {}, false, 'DYN', veSupport);
					if (instance) {
						if (!instance.stateful) {
							// 创建新TAB，并加载组件
							$E.progressing = true;
							$D.live(instance, area);
							$E.progressing = false;
							area.data('instance', instance);
						} else
							$F.alert('有状态动态页面不允许直接打开');
					} else
						$F.err('Project - openAddinDyn', '创建实例失败：' + cs[0]);
				}
			}

			/**
			 * 应答一个呼入请求
			 * @method onAnswerCall
			 */
			onAnswerCall () {
				this.events.notify($CONST.ModuleEvent.ECM_WORKSTART, {
					workStatus : WorkInfo.WT_IB
				})
			}

			onLauncher () {
				const proj = this, dialog = $('#launcherDialog');
				let size = $.cookie('launcher.dialog');
				if (size) {
					size = size.split(',')
					if (size.length == 2) {
						$('.dialogContent', dialog).css({
							width: size[0] + 'px',
							height: size[1] + 'px'
						})
					}
				}
				$('.dialogContent', dialog).resizable({
					minWidth: 600,
					minHeight: 300,
					stop: function( event, ui ) {
						$.cookie('launcher.dialog', ui.size.width + ',' + ui.size.height, {
							expires : 30
						});
					}
				});

				const shortcuts = proj.launchers;
				const addItem = function(ul, bean) {
					const li = $('<li />').data('entry', bean),
						edit = $('<a href="javascript:;" class="edit far fa-fw fa-edit" />').on('click', function() {
							H5Utils.getFontIcon(0, function(e) {
								li.data('entry').icon = e;
								li.find('.icon').html('<i class="fa-fw ' + $F.fas(e) + '" />');
							}, bean.icon)
						}),
						rm = $('<a href="javascript:;" class="rm far fa-fw fa-trash-alt" />').on('click', function(){
							shortcuts.remove(li.data('entry'));
							li.remove();
						});
					li.append('<span class="icon"><i class="fa-fw ' + $F.fas(bean.icon) + '"></i></span>')
						.append('<span class="name">' + bean.name + '</span>')
						.append($('<span class="item-actions" />').append(edit).append(rm));
					ul.append(li);
				};
				let launcherIndex = 0;
				$F.dialogOpen(dialog, {
					onOpen : function() {
						loopMenus(proj.menus, function(menu) {
							if (menu.hidden)
								menu.classes = "display:none";
						});
						// 多层树结构，采用传统风格
						const menutree = $('.menutree', dialog).wisTree('destroy').wisTree({
							lang: $E.lang,
							completely: true,
							showCheck: true
						}).setData(proj.menus);
						const ul = $('.shortcuts', dialog).empty();
						$.each(shortcuts, function(i, bean) {
							addItem(ul, bean);
							launcherIndex = Math.max(launcherIndex, bean.index);
						})
						ul.sortable({
							update: function(event, ui) {
								$.each(ul.find('>li'), function(i, li) {
									$(li).data('entry').index = launcherIndex = i;
								})
							}
						});
						ul.disableSelection();
						$('.js_pick', dialog).off('click').on('click', function(){
							const items = menutree.getChecked();
							menutree.clearCheck();
							$.each(items, function(i, item) {
								if (! shortcuts.contains(item, function(a, b) {
									return (a.id == b.id)
								})) {
									launcherIndex ++;
									const bean = {
										id: item.id,
										name: item.name,
										icon: item.icon,
										index: launcherIndex
									}
									shortcuts.push(bean);
									addItem(ul, bean);
								}
							})
						})
					},
					onSubmit: function(close) {
						$('.shortcuts>li', dialog).each(function(i, li) {
							$(li).data('entry').index = i;
						})
						proj.launchers = shortcuts.sort(function(a, b) {
							return a.index - b.index;
						})
						proj.launchers.forEach((node) => {
							delete node.menu;
						})
						close();
						//console.log(proj.launchers);

						const bd = new BoundData('SYSC_SHORTCUTS' + $E.staff.epidSql);
						bd.KeyInfoEdit('STAFF_ID');
						bd.setValue('staff_id', $E.staff.id);
						bd.setValue('SHORTCUTS', JSON.stringify(proj.launchers));
						bd.setValue('MODIFIEDDATE', new Date().format('yyyy-MM-dd HH:mm:ss'));
						$F.dataService('SystemDo5', {
							data: {
								token: proj.token,
								dbPool: '',
								xml: bd.getData()
							},
							digest: proj.digest,
							success: function(data) {
								if (data.code > 0) {
									$F.notify($F.lang('ui.cfg.shortcutUpdated'));
									proj.initShortcuts();
								} else
									$F.alert(data.message);
							}
						})
					}
				})
			}

			getNav() {
				return $(">.ui-tabs-scroll-container>.ui-tabs-nav-scrollable>.ui-tabs-nav", this.tabs);
			}
			getNavs() {
				return $(">li", this.getNav());
			}

			/**
			 * 创建TAB页面标签
			 *
			 * @method tabCreate
			 * @param id
			 *            {string} TAB ID
			 * @param label
			 *            {string} TAB标签
			 * @param closable
			 *            {boolean} TAB是否允许关闭
			 * @param cacheable
			 * 			  {boolean} TAB是否允许隐藏
			 * @return {dom} 新创建的TAB
			 */
			tabCreate (id, label, closable, cacheable) {
				const a = this,
					m = 'js_tab_' + id,
					nav = a.getNav(),
					container = $("<div id='" + m + "' class='dyn_container' style='border-radius:0'><i class='page-loading fa fa-spinner fa-spin'></i></div>");

				if ($('#' + m, this.tabs).length > 0) {
					$F.alert('创建了相同ID的TAB标签:' + m);
					return $('<div />');
				}

				const tab = $("<li data-id='" + id + "'><a href='#" + m + "'><span class='title'>" + label + '</span>'+ (closable ? "<span class='ui-icon ui-icon-close'></span>" : "") + "</a></li>").appendTo(nav);
				tab.data('closable', closable);
				tab.data('cacheable', cacheable);
				container.data('tab', tab);
				a.tabs.append(container);
				a.tabs.xtabs("refresh");
				const tabIdx = $('li', nav).length;
				a.tabs.xtabs({
					active : tabIdx - 1
				});
				$('.ui-tabs-active', a.tabs)[0].scrollIntoView()
				$('span.ui-icon-close', tab).on("click", function(e) {
					e.preventDefault();
					e.stopPropagation();
					$(document).click();
					// 注册TAB的关闭按钮
					if (!$('#mainContainer').hasClass('locked')) {
						let tab = $(this).closest("li");
						if (tab.data('cacheable'))
							a.tabHide(tab);
						else
							a.tabClose(tab);
					}
				});

				$('#logoArea', this.tabs).remove();
				$('.banner-dmp').hide();
				return tab.data("container", container);
			}

			/**
			 * 在已经打开的TAB中查询相关标识
			 * @method tabFind
			 * @param addinId {string} 组件ID
			 * @return {number} TAB序号，下标为0。返回-1为没有查询到
			 */
			tabFind (addinId) {
				const proj = this, navs = proj.getNavs();
				for (let i = 0; i < navs.length; i++) {
					if ($(navs[i]).data("addinId") == addinId)
						return i;
				}
				return -1;
			}

			tabFindByInstanceId (instanceId) {
				const proj = this, navs = proj.getNavs();
				for (let i = 0; i < navs.length; i++) {
					if (!$(navs[i]).data("instance"))
						$F.err('Project - tabClose', 'TAB中没有找到有效的实例对象');else {
						if ($(navs[i]).data("instance").id == instanceId)
							return $(navs[i]);
					}
				}
			}

			tabHide (v) {
				const proj = this;
				let tab = ($.type(v) === 'string') ? proj.tabFindByInstanceId(v) : v;
				if (!tab) {
					//return $F.alert('没有在已经打开的标签中找到该实例：' + v);
					return;
				}

				$F.log('Project - tabHide', '隐藏已经打开的标签页：' + tab.data('addinId'));
				// 回到上一个页面
				let thisId = tab.attr('data-id'), tabIdx = 0;
				$.each(proj.tabOrders, function(i, v) {
					if (thisId === v) {
						proj.tabOrders.splice(i, 1);
						if (i > 0) {
							tabIdx = proj.tabFindById(proj.tabOrders[i - 1]);
						}
						return false;
					}
				});
				//console.info('Tab left count: ' + proj.tabOrders.length);
				tab.hide();
				tab.data("container").hide();
				this.tabs.xtabs({
					active : tabIdx
				})
			}

			/**
			 * 激活某个tab
			 * @method tabActive
			 * @param tab {dom|string} TAB的标签DOM，或者instance的ID
			 */
			tabActive (tab) {
				const proj = this;
				if ($.type(tab) == 'string')
					tab = proj.tabFindByInstanceId(tab);
				if (!tab)
					return;

				let thisId = tab.attr('data-id');
				$.each(proj.tabOrders, function(i, v) {
					if (thisId == v) {
						proj.tabs.xtabs({
							active : proj.tabFindById(proj.tabOrders[i])
						});
						return false;
					}
				})
			}

			/**
			 * 关闭某个TAB
			 * @method tabClose
			 * @param v {dom|string} TAB的标签DOM，或者instance的ID
			 * @param clear {boolean} 是否关闭后清除struct缓存
			 */
			tabClose (tab, clear) {
				const proj = this;
				if ($.type(tab) === 'string')
					tab = proj.tabFindByInstanceId(tab);
				if (!tab)
					return;

				//cacheable的tab，仅仅去hide，而不是真的close
				if (tab.data('cacheable')) {
					// console.log('hiden tab: ', tab.id)
					proj.tabHide(tab);
					return;
				}
				const instance = tab.data("instance");
				if (!instance)
					return $F.err('Project - tabClose', '被关闭的TAB中没有找到有效的实例对象');

				// 先关闭instance上的embed中的子instance
				if (instance.embed && instance.embed.length > 0) {
					instance.embed.forEach(function(embInstance) {
						proj.closeInstance(embInstance);
					})
				}

				if (!proj.closeInstance(instance)) {
					return;
				}

				// $F.log('Project - tabClose', '关闭已经打开的标签页：' + tab.data('addinId'));

				// 回到上一个页面
				const thisId = tab.attr('data-id'), navs = proj.getNavs();
				$.each(proj.tabOrders, function(i, v) {
					if (thisId == v) {
						//console.log('find tab in tabOrders, removed')
						proj.tabOrders.splice(i, 1);
						//仅当当前页就是要关闭的页时候，才回需要考虑active之前一页
						if (thisId == proj.currentTabId) {
							//console.log('closed tab is current tab')
							while (i > 0) {
								i--;
								let tabIdx = proj.tabFindById(proj.tabOrders[i]);
								if (!$(navs[tabIdx]).is(':hidden')) {
									proj.tabs.xtabs({
										active : tabIdx
									});
									break;
								}
							}
						}
						return false;
					}
				});

				//.ui-tab-panel
				//console.info('Tab left count: ' + proj.tabOrders.length);
				tab.data("container").remove().html('');
				tab.remove().html('');
				proj.tabs.xtabs("refresh");
				if (proj.tabOrders.length === 0)
					$('.banner-dmp').show();
			}

			closeInstance (instance) {
				const proj = this;
				if (instance.type === 'PLUGIN') {
					if ($.isFunction(instance.destroy))
						instance.destroy();
					if ($.isFunction(instance.close))
						instance.close();
				} else if (instance.type === 'WO' || instance.type === 'DYN' || instance.type === 'DYN2' || instance.type === 'OBJ') {
					const R = {
						CLOSABLE : 1
					};
					if (instance.struct.version === 'DYN2')
						instance.doCmdIncludeComponents('close', R);
					else
						instance.doCmdIncludeComponents('P-240', R);
					if (!R.CLOSABLE || R.CLOSABLE <= 0) {
						$F.log('Project - tabClose', '页面回调控制不允许关闭');
						return false;
					}

					proj.removeModuleInstance(instance.addinId, instance.id);
					//proj.events.remove(instance.id);
					instance.destroy();
					instance = null;
				}
				return true;
			}

			tabCloseAll (deep) {
				const proj = this, navs = proj.getNavs();
				// console.log(deep, navs)
				for (let i = 0; i < navs.length; i++) {
					// console.log($(navs[i]).data('closable'))
					if (deep || $(navs[i]).data('closable'))
						proj.tabClose($(navs[i]), false);
				}
				if (deep) this.moduleInstanceMap = {}
			}

			lockScreen () {
				localStorage.setItem('screenStatus', 1)
				$('#lockScreen').show();
				$('#lockScreen .errorTip').html('');
				$('#lockScreen #checkPass').val('');
			}

			hideLockScreen () {
				let password = $('#checkPass').val();
				$F.dataService('JSGenerateServlet2', {
					data : {
						token : $E.staff.current.token,
						dbPool : $E.projects[$E.staff.curProject].dbPool,
						name : "sys.reload.check",
						o : $F.encPwd(password, {enc: $E.enc, pwdEncWithRSA: $E.pwdEncWithRSA, rsaPubKey: $E.rsaPubKey}),
						uid: $E.uid
					},
					digest: $E.staff.current.ordIndex,
					success : function(data) {
						if (data.code > 0) {
							$('#lockScreen').hide();
							localStorage.setItem('screenStatus', 0)
							$F.lockScreenTimer()
						} else if (data.code === -7){
							if(password === '') {
								$('#lockScreen .errorTip').html($F.lang('lockScreen.padEmpty'));
							} else {
								$('#lockScreen .errorTip').html($F.lang('lockScreen.psdError'));
							}
						} else {
							$('#lockScreen .errorTip').html(data.message);
						}
					}
				})
			}

			/**
			 * 关闭所有有状态实例
			 */
			tabCloseAllStateful (customerGuid) {
				const proj = this, navs = proj.getNavs();
				for (let i = 0; i < navs.length; i++) {
					const instance = $(navs[i]).data('instance');
					if ($(navs[i]).data('closable') && instance.stateful) {
						if (customerGuid) {
							//如果传递了customerGuid，则只关闭和这个客户guid关联的页面
							if (instance && instance.customer && instance.customer.getValue('customer_guid') === customerGuid) {
								proj.tabClose($(navs[i]), false);
							}
						} else {
							proj.tabClose($(navs[i]), false);
						}
					}
				}
			}

			tabReopen (instance) {
				const proj = this, navs = proj.getNavs();
				for (let i = 0; i < navs.length; i++) {
					if (!$(navs[i]).data("instance"))
						$F.err('Project - tabReopen', 'TAB中没有找到该实例对象： ' + instance.id);
					else {
						if ($(navs[i]).data("instance").id == instance.id) {
							this.tabReopenByIndex(i);
							return true;
						}
					}
				}
				return false;
			}

			tabReopenByIndex (index) {
				const proj = this, navs = proj.getNavs(), tab = $(navs[index]);
				tab.show();
				tab.data("container").show();
				proj.tabs.xtabs({
					active : index
				})
			}

			tabFindById (tabId) {
				const proj = this, navs = proj.getNavs();
				let idx = 0, li;
				$.each(navs, function(i, v) {
					li = $(v);
					if (li.attr('data-id') == tabId) {
						idx = i;
						return false;
					}
				})
				return idx;
			}

			tabActived () {
				return $(this.tabs.xtabs("instance").active);
			}

			activeCustTab (tabId) {
				const proj = this, navs = proj.getNavs();
				$.each(navs, function(i, v) {
					if ($(v).attr('data-id') == tabId) {
						proj.tabs.xtabs({
							active : i
						});
						return false;
					}
				})
			}

			setAttribute (key, value) {
				this.attributes[key] = value;
			}

			getAttribute (key) {
				return this.attributes[key];
			}

			removeAttribute (key) {
				delete this.attributes[key];
			}

			destroy () {
				const proj = this, navs = proj.getNavs();
				for (let i = 0; i < navs.length; i++) {
					let instance = $(navs[i]).data("instance");
					if (!instance)
						$F.err('Project - destroy', 'TAB中没有找到有效的实例');
					else {
						instance.destroy && instance.destroy();
					}
				}
			}

			/**
			 * 对当前操作的数据进行加锁操作
			 * @method doLock
			 * @param {string} mainId
			 * @param {string} [subId]
			 * @param {string} [dbPool]
			 * @param {Function} [callback] 加锁回调，如果传递回调，则不会执行默认提示
			 * @param {number} [state] 提示类型 0是notify 1是alert
			 * @return {boolean} 加锁是否成功
			 */
			doLock (mainId, subId, dbPool = '', callback, state = 0) {
				const proj = this;
				let lock = false;
				$F.dataService('JSGenerateServlet2', {
					data : {
						token : proj.token,
						dbPool : dbPool,
						name : 'System.Locked',
						mainId : mainId || '',
						subId : subId || ''
					},
					digest : proj.digest,
					async : false,
					success : function(data) {
						lock = data.code > 0;
						if (callback) {
							callback(data);
						} else {
							if (data.code > 0) {
								$F.notify($F.lang('sys.lockSuccess'));
							} else {
								if (data.code == -2) {
									const staff = proj.getStaffById(data.message);
									let message = $F.ognl($F.lang('sys.lockedAlert'), function() {
										return staff ? staff.name : data.message;
									});
									if (state === 1) {
										$F.alert(message);
									} else {
										$F.notify(message, 0);
									}
								} else
									$F.alert(data.message);
							}
						}
					}
				});
				return lock;
			}

			/**
			 * 对当前操作的数据进行解锁操作
			 * @method doUnlock
			 * @param {string} mainId
			 * @param {string} [subId]
			 * @param {string} [dbPool]
			 * @return {boolean} 解锁是否成功
			 */
			doUnlock (mainId, subId, dbPool = '') {
				var proj = this,
					unlock = false;
				$F.dataService('JSGenerateServlet2', {
					data : {
						token : proj.token,
						dbPool : dbPool,
						name : 'System.unLocked',
						mainId : mainId || '',
						subId : subId || ''
					},
					digest : proj.digest,
					async : false,
					success : function(data) {
						if (data.code >= 0) {
							unlock = true;
						} else
							$F.warn($F.lang('sys.unlockFailed'));
					}
				});
				return unlock;
			}

			/**
			 * 对当前操作的数据进行加锁状态检查
			 * @method doLockCheck
			 * @param {string} mainId
			 * @param {string} [subId]
			 * @param {string} [dbPool]
			 * @param {boolean} [raw] 传递true，返回的结果就是接口的直接结果
			 * @return {integer} code 0:未加锁，1:自己加锁，-2:被别人加锁，-1：未知失败
			 */
			doLockCheck (mainId, subId, dbPool = '', raw) {
				var proj = this,
					check = {code : -1};
				$F.dataService('JSGenerateServlet2', {
					data : {
						token : proj.token,
						dbPool : dbPool,
						name : 'System.CheckLocked',
						mainId : mainId || '',
						subId : subId || ''
					},
					digest : proj.digest,
					async : false,
					success : function(data) {
						check = data;
					}
				});
				if (!raw)
					return check.code;
				return check;
			}

			/**
			 * 根据staffId获取Staff对象
			 * @method getStaffById
			 * @param staffId {string} 账号StaffID
			 * @param callback {function} 回调方法
			 * @return {object} staff对象
			 */
			getStaffById (staffId, callback) {
				var proj = this,
					staff,
					staffs = proj.getCache($CONST.CacheName.STAFF),
					agents = proj.getCache($CONST.CacheName.AGENT);
				if (!staffs) {
					staffs = {}, agents = {};
					proj.putCache($CONST.CacheName.STAFF, staffs);
					proj.putCache($CONST.CacheName.AGENT, agents);
				}
				staff = staffs[staffId];
				if (staff && staff.bean) {
					if (callback)
						callback(staff);
					return staff;
				}

				let data;
				if (staffId === $E.staff.id) {
					data = $F.sqlbean('System.Staff.getCurrent', proj.token, []);
				} else {
					data = $F.sqlbean('System.Staff.getById', proj.token, [ $E.staff.epidSql, staffId ]);
				}
				$F.dataService('SystemDo2', {
					data : data,
					digest : proj.digest,
					async : (!!callback),
					success : function(data) {
						if (data.code > 0) {
							if (data.value[0].count > 0) {
								var staffBean = $F.makeJsonBean(data.value[0]);
								staff = {
									id : staffBean.STAFF_ID,
									name : staffBean.STAFFNAME,
									agentId : staffBean.AGENT_ID,
									supervisor : staffBean.ISSUPERVISOR,
									rolegroup : staffBean.DEFAULTROLEGROUP,
									headImg : staffBean.HEADIMG,
									bean : staffBean
								};
								staffs[staff.id] = staff;
								agents[staff.agentId] = staff;
								if ($.isFunction(callback))
									callback(staff);
							}
						} else {
							$F.err('Project - getStaffById', data.message);
						}
					}
				});
				return staff;
			}

			/**
			 * 根据staffId获取Staff对象
			 * @method getStaffByAgentId
			 * @param {string} agentId 账号AgentID
			 * @return {object} staff对象
			 */
			getStaffByAgentId (agentId) {
				var proj = this,
					staff,
					staffs = proj.getCache($CONST.CacheName.STAFF),
					agents = proj.getCache($CONST.CacheName.AGENT);
				if (!staffs) {
					staffs = {}, agents = {};
					proj.putCache($CONST.CacheName.STAFF, staffs);
					proj.putCache($CONST.CacheName.AGENT, agents);
				}
				staff = agents[agentId];
				if (staff)
					return staff;

				$F.dataService('SystemDo2', {
					data : $F.sqlbean('System.Staff.getByAgentId', proj.token, [ $E.staff.epidSql, agentId ]),
					digest : proj.digest,
					async : false,
					success : function(data) {
						if (data.code > 0) {
							if (data.value[0].count > 0) {
								var d = data.value[0].fields[0];
								staff = {
									id : d[1],
									name : d[2],
									agentId : d[0],
									supervisor : d[3],
									rolegroup : d[4],
									headImg : d[5]
								}
								//console.log(staff);
								agents[staff.agentId] = staff;
								staffs[staff.id] = staff;
							}
						} else
							return $F.err('Project - getStaffByAgentId', data.message), null;
					}
				});
				return staff;
			}

			/**
			 * 根据staffId获取Staff名称
			 * @method getStaffNameById
			 * @param {string} staffId 账号ID
			 * @return {string} staff名称
			 */
			getStaffNameById (staffId) {
				var staff = this.getStaffById(staffId);
				if ($F.isNull(staff))
					return '';
				return staff.name;
			}

			/**
			 * 根据rolegroupId获取Rolegroup下的所有账号
			 * @method getStaffsByGroupId
			 * @param {string} rolegroupId 组ID
			 * @return {map} staff表
			 */
			getStaffsByGroupId (rolegroupId) {
				const proj = this;
				let entry = proj.getCache($CONST.CacheName.ROLEGROUPSTAFFS);
				if (!entry) {
					entry = {};
					proj.putCache($CONST.CacheName.ROLEGROUPSTAFFS, entry);
				}
				var staffs = entry[rolegroupId];
				if (staffs)
					return staffs;

				staffs = {};
				$F.dataService('SystemDo2', {
					data : $F.sqlbean('System.RoleGroup.Staff', proj.token, [ rolegroupId ]),
					digest : proj.digest,
					async : false,
					success : function(data) {
						if (data.code > 0) {
							if (data.value[0].count > 0) {
								//console.log(data.value[0].fields)
								$.each(data.value[0].fields, function() {
									staffs[this[0]] = this[1];
								})
								entry[rolegroupId] = staffs;
							}
						} else
							$F.err('Project - getStaffsByGroupId', data.message);
					}
				})
				return staffs;
			}

			/**
			 * 获取Rolegroup表
			 * @method getRolegroups
			 * @return {map} rolegroup表
			 */
			getRolegroups (callback) {
				const proj = this;
				let entry = proj.getCache($CONST.CacheName.ROLEGROUPS);
				if (!entry) {
					entry = {};
					$F.dataService('SystemDo2', {
						data : $F.sqlbean('System.RoleGroup', proj.token, [ $E.staff.epidSql ]),
						digest : proj.digest,
						async : $.isFunction(callback),
						success : function(data) {
							if (data.code > 0) {
								if (data.value[0].count > 0) {
									$.each(data.value[0].fields, function(i, v) {
										entry[v[0]] = v[1];
									})
									proj.putCache($CONST.CacheName.ROLEGROUPS, entry);
									if ($.isFunction(callback))
										callback(entry);
								}
							} else
								$F.err('Project - getRolegroups', data.message);
						}
					})
				} else {
					if ($.isFunction(callback))
						callback(entry);
				}
				return entry;
			}

			getRolegroupById (rolegroupId) {
				const rolegroups = this.getRolegroups(), entry = rolegroups[rolegroupId];
				if (!entry) return null;
				return {
					id : rolegroupId,
					desc : entry
				}
			}

			getRolegroupDescById (rolegroupId) {
				const rolegroups = this.getRolegroups(), entry = rolegroups[rolegroupId];
				if (!entry) return '';
				return entry.trim();
			}

			/**
			 * 获取数据库类型
			 * @param dbpool
			 * @returns
			 */
			getDbType (dbpool) {
				const proj = this;
				let dbtypeCache = proj.getCache('dbtype');
				if (! dbtypeCache) {
					dbtypeCache = {};
					proj.putCache('dbtype', dbtypeCache);
				}
				let dbType = dbtypeCache[proj.id + '_' + dbpool];
				if (! dbType) {
					var e = $F.ajaxSync("DBInfo", {
						token : proj.token,
						dbPool : dbpool || ''
					}, proj.digest);
					if (e) {
						if (e.code > 0) {
							dbType = (e.value && e.value.toUpperCase()) || '';
							dbtypeCache[proj.id + '_' + dbpool] = dbType;
							$F.log('Project - getDbType', 'DBType: ' + dbType);
						} else
							$F.err(e.message)
					}
				}
				return dbType;
			}

			/**
			 * 判断号码是不是外地号码
			 * @param num
			 * @returns {Boolean}
			 */
			isDistantNum (num) {
				const dbType = this.getDbType();
				if (Verify.isMobile(num)) {
					var condition = "telfrom + '%'";
					if ($F.equalsIgnoreCase(dbType, "oracle")) {
						condition = "telfrom || '%'";
					} else if ($F.equalsIgnoreCase(dbType, "mysql")) {
						condition = "concat(telfrom, '%')";
					}
					var ret = this.commonQuerySync("ECore.Project.queryMobileArea", [ $E.staff.epidSql, '\'' + num + '\'', condition ]);
					if (!$F.isNull(ret) && ret.value[0].count > 0) {
						$F.log("Project.isDistantNum", "Number " + num + " is not a distant number.");
					} else {
						$F.log("Project.isDistantNum", "Number " + num + " is a distant number.");
						return true;
					}
				}
				return false;
			}

			/**
			 * 检查是否有自定义权限
			 * @param roleguid
			 * @returns {Boolean}
			 */
			checkRolePassQX (roleguid) {
				if (this.getCacheValue($CONST.CacheName.ROLE_PASS_QX, roleguid)) {
					return true;
				}
				return false;
			}

			loadJobItems () {
				var proj = this, sqlkey;

				// (1) 按staff进行配置查询
				if ($E.staff.epid)
					sqlkey = {'JOBANDMSG':'Job.services.saas.byStaff'}
				else
					sqlkey = {'JOBANDMSG':'Job.services.byStaff'}
				var serviceId = $F.query(sqlkey, proj.token, proj.digest, []);
				if (serviceId) {
					if ($E.debug)
						console.log('Load jobs config by staff: ', serviceId[0]);
					return serviceId[0];
				}

				// (2) staff配置不存在的情况，使用系统参数CTLNTY判断是使用role还是rolegroup进行查询
				var CTLNTY = proj.getParam('CTLNTY');
				if (CTLNTY == 'ROLE') {
					// 按角色进行查询
					if ($E.staff.epid)
						sqlkey = {'JOBANDMSG':'Job.services.saas.byRole'}
					else
						sqlkey = {'JOBANDMSG':'Job.services.byRole'}
					var roles = [];
					$.each(proj.roles, function(i, role) {
						roles.push(role.id)
					})
					$F.dataService('SystemDo2', {
						data : $F.sqlbean(sqlkey, proj.token, [roles]),
						async : false,
						digest : proj.digest,
						success : function(data) {
							if (data.code > 0) {
								if (data.value[0].count > 0) {
									var cs = [];
									$.each(data.value[0].fields, function(i, v) {
										$.each(v[0].split(','), function(o, k) {
											if (! cs.contains(k))
												cs.push(k);
										})
									})
									serviceId = cs.join(',');
								}
							} else
								$F.err('Query', data.message);
						}
					})
					if (serviceId) {
						if ($E.debug)
							console.log('Load jobs config by roles: ', serviceId);
						return serviceId;
					}
				} else {
					if ($E.staff.epid)
						sqlkey = {'JOBANDMSG':'Job.services.saas.byGroup'}
					else
						sqlkey = {'JOBANDMSG':'Job.services.byGroup'}
					serviceId = $F.query(sqlkey, proj.token, proj.digest, []);
					if (serviceId) {
						if ($E.debug)
							console.log('Load jobs config by group: ', serviceId[0]);
						return serviceId[0];
					}
				}

				if ($E.staff.epid)
					sqlkey = {'JOBANDMSG':'Job.services.saas.default'}
				else
					sqlkey = {'JOBANDMSG':'Job.services.default'}
				serviceId = $F.query(sqlkey, proj.token, proj.digest, []);
				if (serviceId) {
					if ($E.debug)
						console.log('Load default jobs config: ', serviceId[0]);
					return serviceId[0];
				}
				if ($E.debug)
					console.log('Empty jobs config defined');
				return '';
			}

			setJobItems (intTime) {
				// console.log(this.jobs);
				var proj = this,
					items = $('ul.job_items', proj.jobContainer).empty(),
					cx = proj.loadJobItems(),
					job,
					li,
					count = 'data-person="0" data-person-expired="0" data-group="0" data-group-expired="0"';
				//console.log(proj.jobs);
				//console.log(proj.params);
				//console.log(cx);

				if (!cx || !intTime) {
					// 无参数时，隐藏任务区
					proj.jobContainer.hide();
				} else {
					var cxs = cx.split(','), vs;
					$.each(cxs, function(i, v) {
						v = v.trim();
						if (! v)
							return;
						vs = v.split('=');
						job = proj.jobs[vs[0]];
						if (job) {
							try {
								job.params = job.params && JSON.parse(job.params);
							} catch (e) {
								console.error(e);
								job.params = "";
							}
							if (vs.length > 1)
								job.desc = vs[1];
							if (!job.app || job.personState || job.groupState) {
								li = $('<li class="' + (job.app ? 'app' : '') + '"></li>').appendTo(items);
								if ($E.theme == 'gfSkin' || $E.theme == 'greenSkin' || $E.theme == 'blueSkin') {
									var link = $('<a href="javascript:;" class="job_item ' + job.clazz + '" data-key="' + job.id + '" data-type="' + job.type + '" data-title="' + job.desc + '"' + count + '></a>')
										.append('<i class="notify"></i>')
										.data('job', job)
										.data('expired', 0)
										.appendTo(li);

									if (job.icon.startsWith('[svg]')) {
										var icon = job.icon.substring(5);
										if (job.params && job.params.type === 1) {
											link.append('<svg class="icon svg-icon win" aria-hidden="true"><use xlink:href="#icon-' + icon + '"></use></svg>')
										} else {
											link.append('<svg class="icon svg-icon" aria-hidden="true"><use xlink:href="#icon-' + icon + '"></use></svg>')
										}
									} else {
										link.append('<i class="icon fa-fw ' + $F.fas(job.icon || 'question-square') + '"/>')
									}
								} else {
									var link = $('<a href="javascript:;" class="job_item ' + job.clazz + '" data-key="' + job.id + '" data-type="' + job.type + '" data-title="' + job.desc + '"' + count + '></a>')
										.append('<i class="icon fa-fw ' + $F.fas(job.icon || 'question-square') + '"/>')
										.append('<i class="notify"></i>')
										.data('job', job)
										.data('expired', 0)
										.appendTo(li);
								}
								if (job.params && job.params.type === 1) {
									link.on('click', function () {
										$('#dynWindow').remove();
										$(".job_item").removeClass("selected");
										$(this).addClass("selected");
										var dynContainer = $('<div id="dynWindow"></div>').appendTo(proj.jobContainer);
										$("#dynWindow").css('height',job.params.height);
										$("#dynWindow").css('width',job.params.width);
										if (job.params.direct === 1) {
											$("#dynWindow").css({'left':'0px','top':'0px','bottom':'0px'});
										} else if (job.params.direct === 2) {
											$("#dynWindow").css({'left':'0px','top':'0px','right':'0px'});
										} else if (job.params.direct === 3) {
											$("#dynWindow").css({'left':'0px','bottom':'0px','right':'0px'});
										} else {
											$("#dynWindow").css({'right':'0px','top':'0px','bottom':'0px'});
										}

										$("#dynWindow").show();
										proj.embDyn(dynContainer, job.params.dynId);

										$(document).bind('click',function(e){
											var e = e || window.event; //浏览器兼容性
											var elem = e.target || e.srcElement;
											while (elem) { //循环判断至跟节点，防止点击的是div子元素
												//if (elem.id && elem.id == 'jobContainer' || elem.id && elem.id == 'dynWindow') {
												if (elem.className && elem.className == 'job_item job_app selected' || elem.id && elem.id == 'dynWindow') {
													return;
												}
												elem = elem.parentNode;
											}
											$('#dynWindow').remove();
											$(".job_item").removeClass("selected");
										});

										//  $('.job_item.job_app:not(.selected)').bind('click',function(e){
										// 	$('#dynWindow').remove();
										// 	$(".job_item").removeClass("selected");
										// });

									})
								}
							}
						} else
							$F.err('Project - setJobItems', '不能识别的任务ID：' + v);
					})

					// 创建提醒消息服务快捷入口
					var jobTip = $('#jobTip'),
						jobItems = proj.jobContainer.find('a.job_item');

					let roleOrGroup = proj.getParam('CTLNTY', '');
					if (roleOrGroup.toUpperCase() === 'ROLE')
						$('li.js_li_group>a>span:eq(0)', jobTip).text('角色：')
					// console.log('Job items: ' + jobItems.length);


					jobItems.off('mouseover').on('mouseover', function(e) {
						var entry = $(e.currentTarget);
						if (!(job.params && job.params.type === 1)) {
							jobTip.css({
								left : entry.offset().left + 10,
								top : $E.theme === 'gfSkin' ? entry.offset().top + 60 : entry.offset().top - 60,
								display: 'flex'
							}).data('evtType', entry.attr('data-type')).state = true;
						}

						if($E.theme === 'gfSkin')
							jobTip.addClass('jobTop');
						var pWork = parseInt(entry.attr('data-person')),
							pExpired = parseInt(entry.attr('data-person-expired')),
							gWork = parseInt(entry.attr('data-group')),
							gExpired = parseInt(entry.attr('data-group-expired')),
							liPerson = $('li.js_li_personal', jobTip),
							liGroup = $('li.js_li_group', jobTip),
							jsPerson = $('a.js_personal', jobTip).data('key', entry.attr('data-key')),
							jsGroup = $('a.js_group', jobTip).data('key', entry.attr('data-key')),
							desc = $('.desc', jobTip);
						// console.log(p);console.log(g);

						// 判断个人与组的使用权限
						job = entry.data('job');
						liPerson.toggle(!job.app || job.personState)
						liGroup.toggle(!job.app || job.groupState)

						var countStr = '<span>' + pWork + '</span>',
							titleStr = $F.lang('jobs.current') + pWork + $F.lang('jobs.expired') + pExpired;
						if (pExpired)
							countStr += '<span class="expired">/' + pExpired + '</span>';
						jsPerson.attr('title', titleStr).find('span.js_count').html(countStr);

						countStr = '<span>' + gWork + '</span>';
						titleStr = $F.lang('jobs.current') + gWork + $F.lang('jobs.expired') + gExpired;
						if (gExpired)
							countStr += '<span class="expired">/' + gExpired + '</span>';
						jsGroup.attr('title', titleStr).find('span.js_count').html(countStr);

						desc.text(entry.attr('data-title'));
					}).off('mouseout').on('mouseout', function(e) {
						jobTip.state = false;
						setTimeout(function() {
							if (!jobTip.state)
								jobTip.hide();
						}, 500)
					}).off('mouseout').on('mouseover', function(e) {
						if (job.params && job.params.type === 1) {
							jobTip.state = false;
							if (!jobTip.state)
								jobTip.hide();
						}
					})
					jobTip.off('mouseover').on('mouseover', function(e) {
						jobTip.state = true;
					}).off('mouseout').on('mouseout', function(e) {
						jobTip.state = false;
						setTimeout(function() {
							if (!jobTip.state)
								jobTip.hide();
						}, 500)
					})
					$('a', jobTip).off('click').on('click', function(e) {
						var this$ = $(e.currentTarget),
							key = this$.data('key'),
							job = proj.jobs[key];
						if (!job) {
							$F.err('Project - jobs', '没有找到对应的JOB：' + key);
							return;
						}

						if (job.conflect && proj.workDuration) {
							$F.err('Project - jobs', '该任务禁止在工作状态中使用：' + key);
							return;
						}

						var jobType = this$.hasClass('js_group') ? 1 : 0;
						if (job.app) {
							$F.log('Project - jobs', "External job: " + key + ", " + job.desc);
							if (job.otid) {
								var ot = proj.getObjectiveType(job.otid);
								if (!ot)
									return $F.alert('市场活动不存在：' + job.otid);
								var addin = proj.getAddin(ot.addinId)
								if (!addin)
									return $F.alert('组件不存在：' + ot.addinId);
								if (addin.type.toUpperCase() == 'DYN')
									proj.openAddinObj(job.desc, addin, ot, undefined, {
										JOBSTATE : job.id,
										JOBPARAM : job.params,
										JOBTYPE : jobType
									}, false);
								else
									return $F.alert('暂时未实现非市场活动的提醒事务：' + addin.type);
							} else if (job.dynid) {
								proj.openAddinDyn(job.dynid, job.desc, {
									JOBSTATE : job.id,
									JOBPARAM : job.params,
									JOBTYPE : jobType
								}, false, 'DYN', job.veSupport == 1);
							} else if (job.plugin) {
								var bean = JSON.parse(job.plugin);
								$E.openPlugin(bean.name||'任务', proj, bean.pluginId, bean.pluginParam, bean.parameters, bean.keepOpen);
							} else
								$F.alert('未定义提醒事务的活动：' + job.desc)
							return;
						} else {
							$F.log('Project - jobs', "Internal job: " + job.id + ", " + $CONST.WorkLoadNotifyEvent[job.id]);
							proj.events.notify($CONST.WorkLoadNotifyEvent[job.id], {
								JOBSTATE : job.id,
								JOBPARAM : job.params,
								JOBTYPE : jobType
							});
						}
					})
				}
				this.refreshJobs(intTime);
			}

			/**
			 * 任务获取定时器执行
			 */
			refreshJobs (intTime) {
				let proj = this;
				if ($E.refreshJobsHandler) {
					$F.log('Project - refreshJobs', '清除当前的任务消息刷新定时器');
					clearTimeout($E.refreshJobsHandler);
				}

				let roleOrGroup = proj.getParam('CTLNTY', '');
				var rj = function() {
					let requestData = {
						token : proj.token,
						sqls : []
					};
					if (roleOrGroup.toUpperCase() === 'ROLE') {
						let roles = [];
						$E.staff.current.roles.forEach(function(role) {
							roles.push(role.id)
						});
						requestData.sqls.push({
							areaKey: 'JOBANDMSG',
							key : 'Job.notice.staff.role',
							params : {
								C1: roles
							}
						});
						requestData.sqls.push({
							areaKey: 'JOBANDMSG',
							key : 'Job.notice.role',
							params : {
								C1: roles
							}
						});
					} else {
						requestData.sqls.push({
							areaKey: 'JOBANDMSG',
							key : 'Job.notice.staff'
						});
						requestData.sqls.push({
							areaKey: 'JOBANDMSG',
							key : 'Job.notice.group'
						});
					}
					$F.dataService('SystemDo2', {
						data : requestData,
						digest : proj.digest,
						success : function(data) {
							// REPORTTYPE： 提醒类型, WORKLOAD: 数量, EXIPREWORKLOAD: 过期数量, CHECKTIME: 检查时间
							// 基础类型：FAX=1, EMAIL=2, OUTBOUND=3, WORKORDER=4, SMS=5, WEB=6
							// console.log(data);
							if (data.code > 0) {
								if (data.value[0].count < 0) {
									$F.err('Project - refreshJobs', $F.lang('db.error') + data.value[0].key);
									return;
								}
								if (data.value[1].count < 0) {
									$F.err('Project - refreshJobs', $F.lang('db.error') + data.value[1].key);
									return;
								}

								var getData = function(fields, key) {
									for (i = 0; i < fields.length; i++) {
										if (fields[i][1] == key)
											return fields[i]
									}
									return null;
								}, i;

								proj.jobContainer.find('a.job_item').each(function() {
									var this$ = $(this), key = parseInt(this$.attr('data-type'));
									// console.log('Refresh job: ' + key)
									var r = getData(data.value[0].fields, key);
									// (key, type, workload, expired)
									if (r)
										refreshJobs(proj, this$, 0, r[2], r[3]);
									else
										refreshJobs(proj, this$, 0, 0, 0);

									r = getData(data.value[1].fields, key);
									if (r)
										refreshJobs(proj, this$, 1, r[2], r[3]);
									else
										refreshJobs(proj, this$, 1, 0, 0);
								});
								$E.refreshJobsHandler = setTimeout(function() {
									rj();
								}, 1000 * intTime);
							} else
								$F.err('Project - refreshJobs', data.message);
						},
						silent : true
					})
				};
				if (intTime > 0)
					rj();
			}

			refreshJobsPromptly () {
				// 刷新任务
				this.refreshJobs(parseInt(this.getParam('JOBSCH') || 0));
			}

			/**
			 * refresh marquee, load notices for owner end rolegroup
			 */
			refreshMarquee (marque) {
				var proj = this, checktime1 = '1900-01-01 00:00:00', checktime2 = $F.getDbTime();
				if ($E.refreshMarqueeHandler) {
					$F.log('Project - refreshMarquee', '清除当前的跑马灯刷新定时器');
					clearTimeout($E.refreshMarqueeHandler);
				}
				if (marque < 1)
					return; // stop refresh;

				var flybox = $('#mainMarquee .flyerbox');
				// 判断是否读取所有有效公告（即便已阅读的公告，也需要读入）
				var msgflg = parseInt(proj.getParam('MSGFLG') || 0),
					querySql = 'Msg.notice.query',
					countSql = 'Msg.notice.count';
				if (msgflg > 0) {
					querySql += '.unlimit';
					//countSql += '.unlimit';
				}

				var items = flybox.marquee({
						click2remove : false
					}),
					msgLink;

				var rj = function() {
					// console.log('Marque running at ' + marque + 'secends.');
					//过滤过期公告
					var msgLinks = $('a[data-id]', items.flyer)
					msgLinks.length && $.each(msgLinks, function () {
						if($(this).attr('deprecatedTime') && Date.parse($(this).attr('deprecatedTime')) < Date.parse(new Date())){
							this.remove()
						}
					})
					$F.dataService('SystemDo2', {
						data : {
							token: proj.token,
							dbPool: '',
							sqls: [{
								areaKey: 'JOBANDMSG',
								key: countSql,
								params: {}
							}, {
								areaKey: 'JOBANDMSG',
								key: querySql,
								params: {
									C1: checktime1,
									C2: checktime2
								}
							}]
						},
						digest : proj.digest,
						success : function(data) {
							// console.log(data);
							if (data.code > 0) {
								// refresh unread count
								if (data.value[0].count > 0) {
									var unread = data.value[0].fields[0][0];
									proj.refreshMsgAlert(unread);
								}

								if (data.value[1].count > 0) {
									$.each(data.value[1].fields, function() {
										//console.log(this)
										msgLink = $('<a href="javascript:;" data-id="' + this[0] + '" deprecatedTime="'+ this[4] +'"><i class="far fa-comment-alt"></i></a>').on('click', function(e) {
											e.preventDefault();
											var this$ = $(e.currentTarget);
											if (this$.data('alert'))
												this$.data('alert').trigger('click');
											else {
												$E.openPlugin($F.lang('plugin.message'), proj, 'message', '', {
													action : 'read',
													target : this$.attr('data-id')
												});
											}
										})
										msgLink.append($('<span}><b>[' + this[2] + ']</b>&nbsp;</span>').text(this[1]))
										if (this[3]) {
											$('>i.fa-comment-alt', msgLink).removeClass('far fa-comment-alt').addClass('fa fa-bolt red')
											// msgLink.addClass('priority');
										}
										items.addItem(msgLink);
									})
								}
							} else
								$F.err('Project - refreshMarquee', data.message);

							$F.dataService('SystemDo2', {
								data : $F.sqlbean({'JOBANDMSG':'Msg.note.query'}, proj.token, [ $E.staff.epidSql, $E.staff.id ]),
								digest : proj.digest,
								success : function(data) {
									// console.log(data);
									if (data.code > 0) {
										if (data.value[0].count < 0) {
											$F.err('Project - refreshMarquee', data.value[0].key);
										} else {
											var unread = data.value[0].fields[0], alertNotes = $('header #c_alert li.notes');
											if (unread == 0) {
												$('header .c_alert>a>i').removeClass('unread');
												alertNotes.html('<span class="empty">' + $F.lang('message.emptyNotes') + '</span>');
											} else {
												$('header .c_alert>a>i').forceClass('unread');
												var link = $('<a href="javascript:;"><i class="far fa-fw fa-comment-exclamation green"></i><span>[</span><span class="red">' + unread + '</span><span>] ' + $F.lang('message.unreadNotes') + '</span></a>');
												alertNotes.html(link);
												link.on('click', function(e) {
													$E.openPlugin($F.lang('plugin.message'), proj, 'message', 'note', {})
												})
											}
										}
									} else
										$F.err('Project - refreshMarquee', data.message);

									// $F.log(proj.name, 'Refresh marquee, next execute plan to after ' + marque + ' seconds')
									$E.refreshMarqueeHandler = setTimeout(function() {
										checktime1 = checktime2;
										checktime2 = $F.getDbTime();
										rj();
									}, 1000 * marque);
								},
								silent : true
							})
						},
						silent : true
					})
				};
				rj();
			}

			refreshMsgAlert (unread) {
				var proj = this, alertNotices = $('header #c_alert li.notices');
				if (unread == 0) {
					alertNotices.html('<span class="empty">' + $F.lang('message.emptyNotices') + '</span>');
					$('header .alert_count').text(0).hide();
				} else {
					var link = $('<a href="javascript:;"><i class="far fa-fw fa-comment-alt-exclamation green"></i><span>[</span><span class="red">' + unread + '</span><span>] ' + $F.lang('message.unreadNotices') + '</span></a>');
					alertNotices.html(link);
					link.on('click', function(e) {
						$E.openPlugin($F.lang('plugin.message'), proj, 'message', '', {})
					})
					$('header .alert_count').text(unread).css('display', 'inline-block');
				}
			}

			disabledRightPanel (e) {
				this.chatSwitch.toggle(e ? false : true);
				this.videoChatSwitch.toggle(e ? false : true);
			}

			isDbMask () {
				return parseInt(this.getParam('DBMASK') || '0') ? true : false
			}

			/**
			 * 对一个codetable表进行query查询
			 * @method queryCodetable
			 * @param tableName {string} 全局SQL名
			 * @param expression {string} JSONSQL查询条件
			 * @return {array}
			 */
			queryCodetable (tableName, expression, options) {
				var a = this;
				a.cache[tableName] || a.loadCodetable(tableName);
				// console.log('JSONSQL Expression: ' + expression, a.cache[tableName]);
				if (expression) {
					expression = expression.replace(/\* where /gi, '* where ');
					var result = jsonSql(a.cache[tableName], expression, options);
					// console.log('JSONSQL[' + tableName + ']: ', expression, result);
					return result;
				} else
					return a.cache[tableName];
			}

			/**
			 * 快速检索返回codetable表的某条记录
			 * @method getCodetableByKey
			 * @param tableName {string} 全局SQL名
			 * @param key {string} 主键值
			 * @return {object}
			 */
			getCodetableByKey (tableName, key) {
				var a = this;
				a.cache[tableName] || a.loadCodetable(tableName);
				return a.cache[tableName][key];
			}

			loadCodetable (tableName) {
				var a = this,
					m = new Date(),
					dataRequest = {
						token : a.token,
						dbPool : '',
						sqls : []
					},
					tnArr = [];

				if ($.type(tableName) == 'string') {
					if (a.cache[tableName])
						return;
					dataRequest.sqls.push({
						pubKey : tableName,
						page : 0,
						maxRow : a.pubSqlMaxRows
					})
				} else {
					if (tableName instanceof Array) {
						tableName.forEach(function(item) {
							if (a.cache[item])
								return;
							dataRequest.sqls.push({
								pubKey : item,
								page : 0,
								maxRow : a.pubSqlMaxRows
							})
							tnArr.push(item);
						})
						if (tnArr.length == 0)
							return;
					} else
						return;
				}
				var result = $F.ajaxSync("CacheDoPubSql2", dataRequest, a.digest);
				// console.log(result);

				if (result.code > 0) {
					result.value.forEach(function(r, ti) {
						var cName = ($.type(tableName) == 'string' ? tableName : tnArr[ti]);
						if (r.count >= 0) {
							// console.info(result.value[0])
							if ($.type(r.fields) == 'array') {
								a.cache[cName] = {};
								$.each($F.makeJsonArray(r), function(i, v) {
									a.cache[cName]['KEY-' + i] = v;
								})
								//console.info('获得' + r.fields.length + '条记录在' + cName + '中');
							} else {
								a.cache[cName] = r.fields;
								var i = 0;
								for (var o in a.cache[cName])
									i++;
								var str = '获得' + i + '条记录在' + cName + '中';
								if (i < r.count) {
									str += ', 其中可能因为主键重复造成' + (r.count - i) + '条记录丢失';
									$F.err('Project - loadCodetable', str);
								}
								// console.info(str);
							}
							$F.log(a.id, 'CodeTable - ' + cName + ', took time: ' + (new Date() - m) + '\'ms')
						} else {
							// console.log(result);
							$F.err('Project - loadCodetable', "全局SQL执行失败: " + cName);
						}
					})
				} else {
					throw new Error("全局SQL执行失败: " + result.message);
				}
			}

			currentStaff () {
				var proj = this;
				if (!proj.currentStaffObject) {
					$F.dataService('SystemDo2', {
						data : $F.sqlbean('System.currentStaff', proj.token, [ $E.staff.epidSql, $E.staff.id ]),
						digest : proj.digest,
						async : false,
						success : function(data) {
							if (data.code > 0) {
								if (data.value[0].count > 0)
									proj.currentStaffObject = $F.makeJsonBean(data.value[0]);
								else
									$F.alert($E.staff.id + ' 不存在');
							} else
								$F.alert(data.message);
						}
					})
				}
				return proj.currentStaffObject;
			}

			currentRolegroup () {
				var proj = this;
				if (!proj.currentRolegroupObject) {
					$F.dataService('SystemDo2', {
						data : $F.sqlbean('System.currentRolegroup', proj.token, [ $E.staff.epidSql, proj.groupId ]),
						digest : proj.digest,
						async : false,
						success : function(data) {
							if (data.code > 0) {
								if (data.value[0].count > 0)
									proj.currentRolegroupObject = $F.makeJsonBean(data.value[0]);
								else
									$F.alert(proj.groupId + ' 不存在');
							} else
								$F.alert(data.message);
						}
					})
				}
				return proj.currentRolegroupObject;
			}

			/**
			 * 根据给定dnis去menus中判断是不是当前项目
			 * menu的matchCti字段配置一个或多个电话号码，多个用分号分隔，如果其中一个和传入dnis匹配，就认为match到
			 * @param dnis
			 * @returns {Boolean}
			 */
			isMatchProject (dnis) {
				var proj = this;
				var menusOpenWhenWorkStart = [];
				loopMenus(proj.menus, function(menu) {
					var matchCti = menu.matchCti;
					if (matchCti) {
						var matchCtiArr = matchCti.split(';');
						$.each(matchCtiArr, function(i, mc) {
							if ($F.equalsIgnoreCase(dnis, mc)) {
								menusOpenWhenWorkStart.push(menu);
								return;
							}
						});
					}
				});
				if (menusOpenWhenWorkStart.length <= 0) {
					return false;
				}
				return true;
			}

			shieldString (string) {
				if (!string)
					return '';

				var TELWLD = this.getParam('TELWLD'),
					TELSHD = this.getParam('TELSHD'),
					TELWLF = this.getParam('TELWLF'),
					TELSHR = this.getParam('TELSHR');
				return $F.shieldNumber(string, TELWLD, TELSHD, TELWLF, TELSHR);
			}
		}

		var JOB_KEY = [ 'FAX', 'MAIL', 'OB', 'WO', 'SMS', 'WEB' ];
		var refreshJobs = function(proj, dom, type, workload, expired) {
				// 如果存在过期任务，则优先凸显过期任务
				var job = dom.data('job'), oldExpired;
				//		if (workload > 0 || expired > 0)
				//			console.log((type?'组':'个人') + job.desc + ': ' + workload + ', ' + expired);
				if (type == 0) {
					// 个人工作任务
					if (job.app && !job.personState)
						return;
					dom.attr('data-person', workload);
					oldExpired = parseInt(dom.attr('data-person-expired'));
					dom.attr('data-person-expired', expired);
				} else { // 组任务
					if (job.app && !job.groupState)
						return;
					dom.attr('data-group', workload);
					oldExpired = parseInt(dom.attr('data-group-expired'));
					dom.attr('data-group-expired', expired);
				}
				var red = Math.max(parseInt(dom.attr('data-person-expired')), parseInt(dom.attr('data-group-expired')));
				if (red) {
					$('i.notify', dom).removeClass('green').show().text(red > 999 ? '999+' : red);
					$('i.icon', dom).forceClass('avaliable');
				} else {
					var green = Math.max(parseInt(dom.attr('data-person')), parseInt(dom.attr('data-group')));
					if (green)
						$('i.notify', dom).forceClass('green').show().text(green > 999 ? '999+' : green);
					else
						$('i.notify', dom).text('').hide();
					$('i.icon', dom).toggleClass(green);
				}

				if (expired > oldExpired) {
					// console.log((type ? '[组]' : '[个人]') + '原有过期数：' + oldExpired + ', 现有过期数：' + expired)
					// 过期数量变化且增多
					if (!job.alertType)
						return;

					if (job.alertType == 1) {
						// alert with dialog
						alert('您有' + expired + '个过期的' + (type ? '组' : '个人') + job.desc + '，请尽快处理。');
					} else if (job.alertType == 2) {
						// alert with sound
						var audio = $('audio#jobAlert');
						if (audio.data('playing'))
							return;
						audio.on('ended', function() {
							audio.off('ended');
							audio.removeData('playing')
						})
						audio[0].play();
						audio.data('playing', true)
					} else if (job.alertType == 3) {
						// alert with popup
						let t = $F.lang('jobs.popup.title'),
							m = $F.lang(type?'jobs.popup.notify':'jobs.popup.notify2').format(expired, job.desc);

						if (job.app) {
							// console.log('Customization job: ' + job.id, job);
							// 自定义任务
							H5Utils.popup({
								title : t,
								message : m,
								eventData : {
									job : job,
									type : type
								},
								callback : function(data) {
									let job = data.job;
									if (job.otid) {
										let ot = proj.getObjectiveType(job.otid);
										if (!ot)
											return $F.alert('市场活动不存在：' + job.otid);
										let addin = proj.getAddin(ot.addinId)
										if (!addin)
											return $F.alert('组件不存在：' + ot.addinId);
										if (addin.type.toUpperCase() == 'DYN')
											proj.openAddinObj(job.desc, addin, ot, undefined, {
												JOBSTATE : job.id,
												JOBPARAM : job.params,
												JOBTYPE : data.type
											}, false, 'DYN', job.veSupport);
										else
											$F.alert('不合法的市场活动定义：' + addin.type)
									} else if (job.dynid) {
										proj.openAddinDyn(job.dynid, job.desc, {
											JOBSTATE : job.id,
											JOBPARAM : job.params,
											JOBTYPE : data.type
										}, false, 'DYN', job.veSupport);
									} else if (job.plugin) {
										let bean = JSON.parse(job.plugin);
										$E.openPlugin(bean.name||'任务', proj, bean.pluginId, bean.pluginParam, bean.parameters, bean.keepOpen);
									} else
										$F.alert('未定义提醒事务的活动：' + job.desc)
								},
								urgency : true
							});
						} else {
							//console.log(job.id + ' in ' + $CONST.WorkLoadNotifyEvent[job.id]);
							H5Utils.popup({
								title : t,
								message: m,
								event : $CONST.WorkLoadNotifyEvent[job.id],
								eventData : {
									JOBSTATE : job.id,
									JOBPARAM : job.params,
									JOBTYPE : type
								},
								urgency : true
							});
						}
					}
				}
			},

			loopMenus = function(menus, callback) {
				menus.forEach(function(menu) {
					if ($.isFunction(callback)) {
						callback(menu);
					}
					if (menu.children && menu.children.length > 0) {
						loopMenus(menu.children, callback);
					}
				});
			},

			getTreeLevel = function(tree, level) {
				var newLevel = level + 1,
					cLevel;
				$.each(tree, function(i, v) {
					if (v.children && v.children.length > 0) {
						cLevel = getTreeLevel(v.children, level + 1);
						newLevel = Math.max(newLevel, cLevel);
					}
				})
				return newLevel;
			},

			getRelationMenu = function(menu, proj) {
				$F.dataService('SystemDo2', {
					data : $F.sqlbean2({MENU:'System.Menu.SAAS.Relation'}, proj.token, proj.getParam('CFGDBP'), [menu.source]),
					digest : proj.digest,
					async: false,
					success : function(data) {
						// console.log(data);
						if (data.code < 0) {
							$F.err('Project - getRelationMenu', data.message);
						} else {
							if (data.value[0].count < 1) {
								$F.err('Project - getRelationMenu', '菜单原型不存在：' + menu.source);
								return;
							}
							var v = data.value[0].fields[0];
							menu = $.extend({}, menu);
							delete menu.source;
							menu.addinId = v[0];
							menu.addinCmd = v[1];
							menu.otId = v[2];
							menu.matchCti = v[3];
							menu.callNatureId = v[4];
							menu.callNatureGroupId = v[5];
							menu.type = v[6];
							menu.singleton = parseInt(v[7] || 0);
						}
					}
				})
				// console.log(menu);
				return menu;
			};

		return {
			create : function(projId, projName, projToken, projDigest, groupId, groupName, roles) {
				return new Project(projId, projName, projToken, projDigest, groupId, groupName, roles);
			}
		};
	});
